Skip to content
Kappa
Code Block
@dicehub/kappav0.6.2

Code Block

Display code with syntax highlighting, language selection, line numbers, and a copy button.

courant-limit.ts
// Clamp the Courant number before each iterationconst courant = computed(() => Math.min(rawCourant.value, 5));
watch(courant, (value) => { solver.setMaxCourant(value);});
<script setup lang="ts">
import { ref } from "vue";
import { CodeBlock, ShikiProvider } from "@dicehub/kappa/components/code-block";

const language = ref("typescript");
const languageOptions = [
  { label: "TypeScript", value: "typescript" },
  { label: "Python", value: "python" },
];
const titles: Record<string, string> = {
  typescript: "courant-limit.ts",
  python: "courant_limit.py",
};
const code: Record<string, string> = {
  typescript: `// Clamp the Courant number before each iteration
const courant = computed(() => Math.min(rawCourant.value, 5));

watch(courant, (value) => {
  solver.setMaxCourant(value);
});`,
  python: `# Clamp the Courant number before each iteration
courant = min(raw_courant, 5)
solver.set_max_courant(courant)`,
};
</script>

<template>
  <ShikiProvider :languages="['typescript', 'python']">
    <CodeBlock
      v-model:lang="language"
      :code="code[language] ?? ''"
      :title="titles[language]"
      :language-options="languageOptions"
      show-copy-button
    />
  </ShikiProvider>
</template>

Language Switch

Use v-model:lang with languageOptions to select a language. Pass the selected source to code and load all choices with ShikiProvider. Use showLineNumbers to add line numbers.

Fetch a simulation run
async function fetchRun(id) {  const response = await fetch(`https://api.example.com/runs/${id}`);  if (!response.ok) throw new Error("Could not fetch run");  return response.json();}
const run = await fetchRun("run-042");console.log(run.status);
<script setup lang="ts">
import { ref } from "vue";
import { CodeBlock, ShikiProvider } from "@dicehub/kappa/components/code-block";

const language = ref("javascript");
const languageOptions = [
  { label: "JavaScript", value: "javascript" },
  { label: "Python", value: "python" },
  { label: "Bash", value: "bash" },
];
const code: Record<string, string> = {
  javascript: `async function fetchRun(id) {
  const response = await fetch(\`https://api.example.com/runs/\${id}\`);
  if (!response.ok) throw new Error("Could not fetch run");
  return response.json();
}

const run = await fetchRun("run-042");
console.log(run.status);`,
  python: `import json
from urllib.request import urlopen

def fetch_run(run_id):
    with urlopen(f"https://api.example.com/runs/{run_id}") as response:
        return json.load(response)

run = fetch_run("run-042")
print(run["status"])`,
  bash: `curl --fail --silent \\
  "https://api.example.com/runs/run-042" \\
  | jq -r '.status'`,
};
</script>

<template>
  <ShikiProvider :languages="['javascript', 'python', 'bash']">
    <CodeBlock
      v-model:lang="language"
      :code="code[language] ?? ''"
      :language-options="languageOptions"
      title="Fetch a simulation run"
      show-line-numbers
      show-copy-button
    />
  </ShikiProvider>
</template>

Tabbed Examples

Use Tabs to switch between request variants. Each example keeps the selected language.

Basic request
const response = await fetch("https://api.example.com/runs");const runs = await response.json();console.log(runs);
<script setup lang="ts">
import { ref } from "vue";
import { Tabs } from "@dicehub/kappa";
import { CodeBlock, ShikiProvider } from "@dicehub/kappa/components/code-block";

const language = ref("typescript");
const languages = [
  { label: "TypeScript", value: "typescript" },
  { label: "Python", value: "python" },
];
const examples: { value: string; label: string; code: Record<string, string> }[] = [
  {
    value: "simple",
    label: "Basic request",
    code: {
      typescript: `const response = await fetch("https://api.example.com/runs");
const runs = await response.json();
console.log(runs);`,
      python: `import json
from urllib.request import urlopen

with urlopen("https://api.example.com/runs") as response:
    runs = json.load(response)
print(runs)`,
    },
  },
  {
    value: "filtered",
    label: "With filters",
    code: {
      typescript: `const query = new URLSearchParams({ status: "completed", limit: "10" });
const response = await fetch(\`https://api.example.com/runs?\${query}\`);
const runs = await response.json();`,
      python: `import json
from urllib.parse import urlencode
from urllib.request import urlopen

query = urlencode({"status": "completed", "limit": 10})
with urlopen(f"https://api.example.com/runs?{query}") as response:
    runs = json.load(response)`,
    },
  },
];
</script>

<template>
  <ShikiProvider :languages="['typescript', 'python']">
    <Tabs.Root default-value="simple" lazy-mount unmount-on-exit>
      <Tabs.List aria-label="Request examples">
        <Tabs.Trigger v-for="example in examples" :key="example.value" :value="example.value">
          {{ example.label }}
        </Tabs.Trigger>
        <Tabs.Indicator />
      </Tabs.List>
      <Tabs.Content v-for="example in examples" :key="example.value" :value="example.value">
        <CodeBlock
          v-model:lang="language"
          :title="example.label"
          :code="example.code[language] ?? ''"
          :language-options="languages"
          show-line-numbers
          show-copy-button
        />
      </Tabs.Content>
    </Tabs.Root>
  </ShikiProvider>
</template>

Installation

Plain text requires only @dicehub/kappa and Vue. For syntax highlighting, also install shiki, @shikijs/langs, and@shikijs/themes. ShikiProvider loads them when it mounts.

Import CodeBlock through its dedicated entry point. The root@dicehub/kappa barrel does not export it.

CodeBlock replaces the name CodeHighlighted. Existing imports from @dicehub/kappa/components/code-highlighted still work.

Dedicated Entry Point

import {
  CodeBlock,
  CodeBlockRoot,
  ShikiProvider,
  useShikiHighlighter,
} from "@dicehub/kappa/components/code-block";

Usage

Use CodeBlock directly for plain text. For syntax highlighting, wrap blocks in one ShikiProvider per application or subtree and set each block's lang. The provider loads the listed languages once. Blocks stay readable as plain text while it loads.

Code uses a system monospace font. Set --kappa-font-mono on the block or a parent to use another font.

interface BoundaryPatch {  name: string;  type: "inlet" | "outlet" | "wall";  faces: number;}
<script setup>
import { CodeBlock, ShikiProvider } from "@dicehub/kappa/components/code-block";

const code = `interface BoundaryPatch {
  name: string;
  type: "inlet" | "outlet" | "wall";
  faces: number;
}`;
</script>

<template>
  <ShikiProvider :languages="['typescript']">
    <CodeBlock :code="code" lang="typescript" />
  </ShikiProvider>
</template>

Composition

Typical part hierarchy
CodeBlock.Provider
└── CodeBlock.Root

Shiki owns tokenization with TextMate grammars. Kappa owns the provider lifecycle (lazy load, disposal, language sets), the fixed light and dark themes, the display geometry, and semantic tokens. Themes are fixed — github-light andvesper — for consistent rendering in both modes.

  • ShikiProvider loads the engine, themes, and languages once, then shares them.
  • CodeBlock renders plain text without a provider. With a provider, unknown languages fall back to plain text with a warning.
  • useShikiHighlighter exposes the shared highlighter for custom blocks and requires a provider.

Examples

Plain Text

Pass code to display commands or logs without a provider or Shiki dependencies. Copy, line numbers, wrapping, and collapse remain available. The default language is text; plain and txtalso keep text unhighlighted inside a provider.

Build output
$ pnpm buildBuilding application...
12 modules transformed.Build complete.
<script setup lang="ts">
import { CodeBlock } from "@dicehub/kappa/components/code-block";

const output = `$ pnpm build
Building application...

12 modules transformed.
Build complete.`;
</script>

<template>
  <CodeBlock title="Build output" :code="output" show-copy-button />
</template>

Title

Use title for a file name or label. The language selector and copy button appear on the right.

mesh-partition.ts
function decompose(mesh: Mesh, partitions: number) {  const regions = mesh.split(partitions);  const balanced = balance(regions);  return balanced.map(assignWeights);}
<CodeBlock
  title="mesh-partition.ts"
  :code="code"
  lang="typescript"
  show-copy-button
/>

Use the title slot to add an icon. Set the title prop to name the code region.

Terminal
$ pnpm add @dicehub/kappaPackages installed.Done.
<script setup lang="ts">
import { TerminalIcon } from "@lucide/vue";
import { CodeBlock } from "@dicehub/kappa/components/code-block";

const output = `$ pnpm add @dicehub/kappa
Packages installed.
Done.`;
</script>

<template>
  <CodeBlock title="Terminal" :code="output" show-copy-button>
    <template #title>
      <TerminalIcon aria-hidden="true" />
      Terminal
    </template>
  </CodeBlock>
</template>

Actions

Use the actions slot for a wrap toggle and download link. showCopyButton adds the copy control.

Terminal
$ pnpm lintsrc/pages/runs.vue  8:15  error    'runs' is possibly 'undefined'. Check the response before reading the first run.  24:7  warning  'selectedRun' is declared but its value is never read.
2 problems (1 error, 1 warning)
<script setup lang="ts">
import { ref } from "vue";
import { DownloadIcon, TerminalIcon, WrapTextIcon } from "@lucide/vue";
import { Button, LinkButton } from "@dicehub/kappa";
import { CodeBlock } from "@dicehub/kappa/components/code-block";

const wrap = ref(true);
const output = `$ pnpm lint
src/pages/runs.vue
  8:15  error    'runs' is possibly 'undefined'. Check the response before reading the first run.
  24:7  warning  'selectedRun' is declared but its value is never read.

2 problems (1 error, 1 warning)`;
const downloadHref = `data:text/plain;charset=utf-8,${encodeURIComponent(output)}`;
</script>

<template>
  <CodeBlock title="Terminal" :code="output" :wrap="wrap" show-copy-button>
    <template #title>
      <TerminalIcon aria-hidden="true" />
      Terminal
    </template>
    <template #actions>
      <Button
        variant="ghost"
        size="xs"
        shape="square"
        :icon="WrapTextIcon"
        aria-label="Wrap lines"
        :aria-pressed="wrap"
        @click="wrap = !wrap"
      />
      <LinkButton
        :href="downloadHref"
        download="lint.log"
        variant="ghost"
        size="xs"
        shape="square"
        :icon="DownloadIcon"
        aria-label="Download output"
      />
    </template>
  </CodeBlock>
</template>

Languages

Load exactly the languages an application needs; aliases such as ts or sh resolve automatically.

TypeScript
async function fetchRun(id: string): Promise<SimulationRun> {  const response = await fetch(`/api/runs/${id}`);  return response.json();}
Vue
<script setup lang="ts">const cells = ref(2_400_000);</script>
<template> <span>{{ cells }} cells</span></template>
Bash
# Build the library and run the docspnpm --filter @dicehub/kappa buildpnpm dev
JSON
{  "solver": "pimpleFoam",  "correctors": 6,  "adaptive": true}
CSS
.kappa-code-block {  border: 1px solid var(--kappa-line);  border-radius: 0.5rem;}
<ShikiProvider :languages="['typescript', 'vue', 'bash', 'json', 'css']">
  <CodeBlock :code="fetchRunSource" lang="typescript" />
  <CodeBlock :code="sfcSource" lang="vue" />
  <CodeBlock :code="installCommands" lang="bash" />
  <CodeBlock :code="solverConfig" lang="json" />
  <CodeBlock :code="tokenStyles" lang="css" />
</ShikiProvider>

File Tabs

Put a code block in each tab to show related files.

client.ts
export async function fetchRun(id: string) {  const response = await fetch(`/api/runs/${id}`);  if (!response.ok) throw new Error("Could not fetch run");  return response.json();}
<script setup lang="ts">
import { Tabs } from "@dicehub/kappa";
import { CodeBlock, ShikiProvider } from "@dicehub/kappa/components/code-block";

const files = [
  {
    name: "client.ts",
    lang: "typescript",
    code: `export async function fetchRun(id: string) {
  const response = await fetch(\`/api/runs/\${id}\`);
  if (!response.ok) throw new Error("Could not fetch run");
  return response.json();
}`,
  },
  {
    name: "config.json",
    lang: "json",
    code: `{
  "solver": "pimpleFoam",
  "partitions": 8,
  "adaptive": true
}`,
  },
  {
    name: "run.sh",
    lang: "bash",
    code: `#!/usr/bin/env bash
set -euo pipefail
curl --fail --silent "https://api.example.com/runs/run-042"`,
  },
];
</script>

<template>
  <ShikiProvider :languages="['typescript', 'json', 'bash']">
    <Tabs.Root default-value="client.ts" lazy-mount unmount-on-exit>
      <Tabs.List aria-label="Example files">
        <Tabs.Trigger v-for="file in files" :key="file.name" :value="file.name">
          {{ file.name }}
        </Tabs.Trigger>
        <Tabs.Indicator />
      </Tabs.List>
      <Tabs.Content v-for="file in files" :key="file.name" :value="file.name">
        <CodeBlock :title="file.name" :code="file.code" :lang="file.lang" show-line-numbers show-copy-button />
      </Tabs.Content>
    </Tabs.Root>
  </ShikiProvider>
</template>

Wrap Lines

Set wrap to fit long lines to the block. Use the actions slot to add a wrap toggle or other header controls.

Fetch simulation results
const endpoint = "https://api.example.com/projects/wind-tunnel/runs/run-042/results?fields=pressure,velocity,turbulence&format=json";
const response = await fetch(endpoint);const results = await response.json();
<script setup lang="ts">
import { ref } from "vue";
import { Button } from "@dicehub/kappa";
import { CodeBlock, ShikiProvider } from "@dicehub/kappa/components/code-block";

const wrap = ref(false);
const code = `const endpoint = "https://api.example.com/projects/wind-tunnel/runs/run-042/results?fields=pressure,velocity,turbulence&format=json";

const response = await fetch(endpoint);
const results = await response.json();`;
</script>

<template>
  <ShikiProvider :languages="['typescript']">
    <CodeBlock title="Fetch simulation results" :code="code" lang="typescript" :wrap="wrap" show-line-numbers show-copy-button>
      <template #actions>
        <Button variant="ghost" size="sm" :aria-pressed="wrap" @click="wrap = !wrap">
          Wrap lines
        </Button>
      </template>
    </CodeBlock>
  </ShikiProvider>
</template>

Collapsible

Add collapsible to show a short preview. Set collapsedLinesto change the preview from ten rows. Short blocks stay fully visible. Copy always copies the full source, including hidden lines.

Use defaultOpen to start expanded, or v-model:open to control the state. Set expandLabel and collapseLabelto change the button text.

fetch-runs.ts
export async function fetchRuns(projectId: string) {  const query = new URLSearchParams({ projectId });  const response = await fetch(`/api/runs?${query}`);
if (!response.ok) { throw new Error("Could not fetch runs"); }
const runs = await response.json(); return runs.filter((run) => run.status === "completed");}
const runs = await fetchRuns("wind-tunnel");console.log(runs);
<script setup lang="ts">
import { CodeBlock, ShikiProvider } from "@dicehub/kappa/components/code-block";

const code = `export async function fetchRuns(projectId: string) {
  const query = new URLSearchParams({ projectId });
  const response = await fetch(\`/api/runs?\${query}\`);

  if (!response.ok) {
    throw new Error("Could not fetch runs");
  }

  const runs = await response.json();
  return runs.filter((run) => run.status === "completed");
}

const runs = await fetchRuns("wind-tunnel");
console.log(runs);`;
</script>

<template>
  <ShikiProvider :languages="['typescript']">
    <CodeBlock
      title="fetch-runs.ts"
      :code="code"
      lang="typescript"
      collapsible
      :collapsed-lines="6"
      show-line-numbers
      show-copy-button
      wrap
    />
  </ShikiProvider>
</template>

Highlight Lines

Emphasize 1-indexed lines with highlightLines.

function decompose(mesh: Mesh, partitions: number) {  const regions = mesh.split(partitions);  const balanced = balance(regions);  return balanced.map(assignWeights);}
<CodeBlock :code="code" lang="typescript" :highlight-lines="[3]" />

Custom Highlight Color

Override the --kappa-code-highlight-bg token to recolor highlighted lines.

function decompose(mesh: Mesh, partitions: number) {  const regions = mesh.split(partitions);  const balanced = balance(regions);  return balanced.map(assignWeights);}
<template>
  <CodeBlock
    class="accent-lines"
    :code="code"
    lang="typescript"
    :highlight-lines="[3]"
  />
</template>

<style scoped>
.accent-lines {
  --kappa-code-highlight-bg: color-mix(in oklab, var(--kappa-accent, #4356e8) 14%, transparent);
}
</style>

Line Numbers

Line numbers render for multi-line blocks and stay out of the selection.

import { computed, watch } from "vue";
export function useCourantLimit(raw: Ref<number>) { const clamped = computed(() => Math.min(raw.value, 5));
watch(clamped, (value) => { solver.setMaxCourant(value); });
return clamped;}
<CodeBlock :code="code" lang="typescript" show-line-numbers />

Copy Button

showCopyButton adds the copy affordance. Single-line blocks keep the button visible inline instead of floating it on hover.

pnpm add @dicehub/kappa
<CodeBlock code="pnpm add @dicehub/kappa" lang="bash" show-copy-button />

Copy Button Labels

The copy button text comes from the provider labels; a block can override them through its own labels prop.

interface BoundaryPatch {  name: string;  type: "inlet" | "outlet" | "wall";  faces: number;}
<ShikiProvider :languages="['typescript']" :labels="{ copy: 'Kopieren', copied: 'Kopiert!' }">
  <CodeBlock :code="code" lang="typescript" show-copy-button />
</ShikiProvider>

Accessibility

  • Code renders as a pre/code region; plain text remains available while the highlighter loads.
  • The optional title renders as a semantic figcaption.
  • Line numbers are decorative and hidden from assistive technology.
  • The code region uses left-to-right text and supports keyboard scrolling.
  • Use Left and Right arrow keys to switch tabs, and Home or End to select the first or last tab.
  • Use arrow keys to select a language. Escape closes the list and returns focus to the selector.
  • The Ark UI copy button has an accessible name and a polite status announcement.
  • The button stays visible on touch devices and remains reachable with the keyboard.
  • Expand and collapse with Enter or Space. The control exposes its state with aria-expanded; the collapsed code region is outside the tab order.

API Reference

ShikiProvider

PropTypeDefaultDescription
languagesstring[]requiredLanguages to load; only these get highlighted.
engine"javascript" | "wasm""javascript"Shiki regex engine; JavaScript is smaller, Oniguruma (wasm) is more accurate.
labelsCodeBlockLabelsCopy / Copied! / Code languageCopy-button and language-selector labels for every CodeBlock inside.

CodeBlock

PropTypeDefaultDescription
codestringrequiredSource text to display.
langLanguageInput | string"text"Language id or alias. text, plain, and txt stay plain; other languages use the provider when available.
languageOptionsCodeBlockLanguageOption[][]Language choices shown in the header; use with v-model:lang.
titlestring—Optional file name or short label rendered above the code.
showLineNumbersbooleanfalseAdds a line-number column for multi-line code.
highlightLinesnumber[][]1-indexed lines to emphasize.
showCopyButtonbooleanfalseShows the copy button; always visible on single-line blocks.
wrapbooleanfalseWraps long lines to fit the block. Line numbers stay aligned with source lines.
collapsiblebooleanfalseShows a preview with Expand code / Collapse code controls when the content exceeds collapsedLines.
collapsedLinesnumber10Visible rows in the collapsed preview, including wrapped rows. Values are rounded down, with a minimum of 1; non-finite values use 10.
defaultOpenbooleanfalseInitial expanded state of a collapsible block.
openboolean—Controlled expanded state; use v-model:open.
expandLabelstringExpand codeText and accessible name of the expand control.
collapseLabelstringCollapse codeText and accessible name of the collapse control.
labelsCodeBlockLabelsproviderOverrides provider labels for this block.

Slots

title replaces the title text with custom content, such as an icon and label. The title prop still names the code region.

actions adds header controls before the copy button. Give each control an accessible name.

Events

update:lang emits the selected language. update:open emits the requested expanded state; openChange emits { open: boolean }.

Exports

ExportDescription
CodeBlockCompound API exposing Root and Provider.
CodeBlockRootUnaugmented code display component.
ShikiProviderLazy-loads one shared Shiki highlighter for all child blocks.
useShikiHighlighterAccess highlight, loading state, and labels inside a provider.
normalizeCodeBlockLanguageResolves aliases to supported language ids.
DEFAULT_CODE_BLOCK_LABELSDefault copy-button and language-selector labels.
CodeBlockLabelsCopy-button and language-selector label overrides.
CodeBlockEmitsEvents, including update:lang for language selection.
CodeBlockLanguageOptionA language choice with a label and value.
CodeBlockPropsPublic component props.
LanguageAliasSupported shorthand language ids.
LanguageInputSupported language ids and aliases.
ShikiEngineSupported Shiki engine names.
ShikiProviderPropsProvider props.
SupportedLanguageLanguages available through the dedicated entry point.
UseShikiHighlighterResultReturn type of useShikiHighlighter.