f-ui
Components

Code Block

Shiki highlighting, line numbers, copy, and scroll area for long snippets. Not tied to Markdown.

Plus Registry

This component ships from registry.plus.json, not the public registry.json. Set up @f-ui-plus and a token on the Installation page, then install @f-ui-plus/code-block below.

The code-block registry item provides CodeBlock: syntax highlighting (Shiki), optional chrome (title bar), line numbers with a sticky gutter on horizontal scroll, optional word wrap, copy, and a scrollable body for long files. Source stays visible as plain monospace until highlight is ready (plain-first); pass loading only to force a spinner for demos. Use it anywhere you need a polished code viewer; markdown-renderer can compose it for fenced blocks.

Scroll / copy layout (Chakra CodeBlock / GitHub-class): copy chrome lives outside the overflow-auto scrollport (header when chrome, absolute overlay when minimal). One scrollport owns both axes so the horizontal scrollbar stays pinned while you scroll vertically; sticky gutters pin on that same scrollport. Path titles truncate the directory and keep the basename; an optional file-type icon sits left of the label.

For rendered diagram previews (image view, zoom, pan, export), use the dedicated Mermaid Renderer component—not CodeBlock alone.

Installing

Configure @f-ui-plus and FUI_PLUS_REGISTRY_TOKEN as in Installation — Plus Registry.

FUI_PLUS_REGISTRY_TOKEN=xxx pnpm dlx shadcn@latest add @f-ui-plus/code-block
FUI_PLUS_REGISTRY_TOKEN=xxx npx shadcn@latest add @f-ui-plus/code-block
FUI_PLUS_REGISTRY_TOKEN=xxx yarn dlx shadcn@latest add @f-ui-plus/code-block
FUI_PLUS_REGISTRY_TOKEN=xxx bun x shadcn@latest add @f-ui-plus/code-block

registryDependencies: copy-affordance, file-type-icon. Runtime: shiki, next-themes (for light/dark theme alignment).

Usage

import { CodeBlock } from "@/components/f-ui/code-block/code-block";

export function Example() {
  return (
    <CodeBlock
      source={'console.log("hi")'}
      language="ts"
      title="example.ts"
    />
  );
}

Examples

Minimal (Default)

import { Hono } from "hono";
import { cors } from "hono/cors";
import { z } from "zod";

const app = new Hono();

app.use("/*", cors());

const schema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});

app.post("/users", async (c) => {
  const body = await c.req.json();
  const parsed = schema.safeParse(body);

  if (!parsed.success) {
    return c.json({ error: parsed.error.flatten() }, 400);
  }

  const user = await db.users.create({
    data: parsed.data,
  });

  return c.json(user, 201);
});

export default app;
"use client";

import { CodeBlock } from "@/components/f-ui/code-block/code-block";

import { HONO_SAMPLE } from "@/demos/code-block/samples";

export function CodeBlockMinimalDemo() {
  return <CodeBlock source={HONO_SAMPLE} language="ts" />;
}

Chrome + Line Numbers

Long path titles truncate the directory with an ellipsis while preferring the basename; when the name alone is too long it can ellipsis too. An optional file-type icon appears left of the label when a title is set.

examples/powerapp/web/src/server.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import { z } from "zod";

const app = new Hono();

app.use("/*", cors());

const schema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});

app.post("/users", async (c) => {
  const body = await c.req.json();
  const parsed = schema.safeParse(body);

  if (!parsed.success) {
    return c.json({ error: parsed.error.flatten() }, 400);
  }

  const user = await db.users.create({
    data: parsed.data,
  });

  return c.json(user, 201);
});

export default app;
"use client";

import { CodeBlock } from "@/components/f-ui/code-block/code-block";

import { HONO_SAMPLE } from "@/demos/code-block/samples";

export function CodeBlockChromeLineNumbersDemo() {
  return (
    <CodeBlock
      source={HONO_SAMPLE}
      language="ts"
      title="examples/powerapp/web/src/server.ts"
      chrome
      lineNumbers
      copyStyle="always"
    />
  );
}

Long Lines (Sticky Gutter on Horizontal Scroll)

router.ts
import { createRouter, createWebHistory, type RouteRecordRaw, type NavigationGuardNext, type RouteLocationNormalized } from "vue-router";
import { useAuthStore } from "@/stores/auth";

const routes: RouteRecordRaw[] = [
  { path: "/", component: () => import("@/views/Home.vue"), meta: { requiresAuth: true, roles: ["admin", "editor", "viewer"], breadcrumb: "Dashboard" } },
  { path: "/settings/profile/notifications/email-preferences", component: () => import("@/views/settings/EmailPreferences.vue"), meta: { title: "Email Notification Preferences — Settings" } },
];

export const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes, scrollBehavior: (to, from, savedPosition) => savedPosition ?? { top: 0, behavior: "smooth" } });
"use client";

import { CodeBlock } from "@/components/f-ui/code-block/code-block";

import { LONG_LINE_ROUTER_SAMPLE } from "@/demos/code-block/samples";

/** Long lines: sticky line-number gutter while code scrolls horizontally. */
export function CodeBlockStickyGutterDemo() {
  return (
    <CodeBlock
      source={LONG_LINE_ROUTER_SAMPLE}
      language="ts"
      title="router.ts"
      chrome
      lineNumbers
      copyStyle="always"
    />
  );
}

Word Wrap + Line Numbers

router.ts
import { createRouter, createWebHistory, type RouteRecordRaw, type NavigationGuardNext, type RouteLocationNormalized } from "vue-router";import { useAuthStore } from "@/stores/auth";
const routes: RouteRecordRaw[] = [  { path: "/", component: () => import("@/views/Home.vue"), meta: { requiresAuth: true, roles: ["admin", "editor", "viewer"], breadcrumb: "Dashboard" } },  { path: "/settings/profile/notifications/email-preferences", component: () => import("@/views/settings/EmailPreferences.vue"), meta: { title: "Email Notification Preferences — Settings" } },];
export const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes, scrollBehavior: (to, from, savedPosition) => savedPosition ?? { top: 0, behavior: "smooth" } });
"use client";

import { CodeBlock } from "@/components/f-ui/code-block/code-block";

import { LONG_LINE_ROUTER_SAMPLE } from "@/demos/code-block/samples";

export function CodeBlockWordWrapDemo() {
  return (
    <CodeBlock
      source={LONG_LINE_ROUTER_SAMPLE}
      language="ts"
      title="router.ts"
      chrome
      lineNumbers
      wordWrap
      copyStyle="always"
    />
  );
}

Loading (Forced Spinner)

The demo below sets loading to force the spinner (demos only). Normal usage keeps plain source visible until highlight is ready.

Pass loading to show the same spinner the component uses while Shiki initializes—no duplicate markup.

example.ts
Loading syntax highlight
import { CodeBlock } from "@/components/f-ui/code-block/code-block"

const SOURCE = 'console.log("hello")'

/** Uses `loading` so the doc demo does not duplicate CodeBlock internals. */
export function CodeBlockLoadingDemo() {
  return (
    <div className="space-y-3">
      <p className="text-muted-foreground text-sm">
        Pass <code className="rounded bg-muted px-1 py-0.5 font-mono text-xs">loading</code> to
        show the same spinner the component uses while Shiki initializes—no duplicate markup.
      </p>
      <CodeBlock
        source={SOURCE}
        language="ts"
        title="example.ts"
        chrome
        loading
      />
    </div>
  )
}

Mermaid (Highlighted Source)

CodeBlock with language="mermaid" behaves like any other language: Shiki highlighting, copy, scroll, and optional chrome or line numbers.

Preview vs. Source Only

This shows syntax-highlighted Mermaid text only. For an interactive diagram preview (Image/Code toggle, toolbar, fullscreen), use Mermaid Renderer.

flowchart LR
  A[Start] --> B{Choice}
  B -->|Yes| C[OK]
  B -->|No| D[End]
"use client";

import { CodeBlock } from "@/components/f-ui/code-block/code-block";

import { MERMAID_SAMPLE } from "@/demos/code-block/samples";

export function CodeBlockMermaidDemo() {
  return <CodeBlock source={MERMAID_SAMPLE} language="mermaid" />;
}

API Reference

PropTypeDescription
sourcestringRaw source (copied on Copy).
languagestringShiki language id or alias (rust, c++, go, …). Warm-loads mainstream languages; any other Shiki bundled grammar loads on demand. Omit for plain monospace.
titlestringHeader label when chrome is on; overrides language in the header.
showFileIconbooleanChrome only. Default on when title is set; off for language-only chrome. Uses File Type Icon.
chromebooleanShow title bar (default false).
lineNumbersbooleanShow gutter (default false).
lineStartnumberFirst line number (default 1).
wordWrapbooleanSoft-wrap long lines (default false).
showCopybooleanShow copy control (default true).
copyStyle"hover" | "always"Minimal (no chrome) overlay visibility (default hover). With chrome, copy always sits in the non-scrolling header.
maxHeightClassNamestringTailwind class for scroll region height (default max-h-[min(70vh,28rem)]).
classNamestringOuter container.
classNamesCodeBlockClassNamesOptional slots: root, header, titleRow, scroll, body, pre.
loadingbooleanIf true, forces a spinner and hides source (demos only).

While Shiki (or an on-demand grammar) is warming, the block shows plain monospace source — not a spinner — then swaps to highlighted HTML once ready.

On this page