// 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.
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.
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
CodeBlock.Provider
└── CodeBlock.RootShiki 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.
ShikiProviderloads the engine, themes, and languages once, then shares them.CodeBlockrenders plain text without a provider. With a provider, unknown languages fall back to plain text with a warning.useShikiHighlighterexposes 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.
$ 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.
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
/>Header
Use the title slot to add an icon. Set the title prop to name the code region.
$ 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.
<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.
async function fetchRun(id: string): Promise<SimulationRun> { const response = await fetch(`/api/runs/${id}`); return response.json();}<script setup lang="ts">const cells = ref(2_400_000);</script>
<template> <span>{{ cells }} cells</span></template># Build the library and run the docspnpm --filter @dicehub/kappa buildpnpm dev{ "solver": "pimpleFoam", "correctors": 6, "adaptive": true}.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.
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.
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.
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/coderegion; 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
| Prop | Type | Default | Description |
|---|---|---|---|
languages | string[] | required | Languages to load; only these get highlighted. |
engine | "javascript" | "wasm" | "javascript" | Shiki regex engine; JavaScript is smaller, Oniguruma (wasm) is more accurate. |
labels | CodeBlockLabels | Copy / Copied! / Code language | Copy-button and language-selector labels for every CodeBlock inside. |
CodeBlock
| Prop | Type | Default | Description |
|---|---|---|---|
code | string | required | Source text to display. |
lang | LanguageInput | string | "text" | Language id or alias. text, plain, and txt stay plain; other languages use the provider when available. |
languageOptions | CodeBlockLanguageOption[] | [] | Language choices shown in the header; use with v-model:lang. |
title | string | — | Optional file name or short label rendered above the code. |
showLineNumbers | boolean | false | Adds a line-number column for multi-line code. |
highlightLines | number[] | [] | 1-indexed lines to emphasize. |
showCopyButton | boolean | false | Shows the copy button; always visible on single-line blocks. |
wrap | boolean | false | Wraps long lines to fit the block. Line numbers stay aligned with source lines. |
collapsible | boolean | false | Shows a preview with Expand code / Collapse code controls when the content exceeds collapsedLines. |
collapsedLines | number | 10 | Visible rows in the collapsed preview, including wrapped rows. Values are rounded down, with a minimum of 1; non-finite values use 10. |
defaultOpen | boolean | false | Initial expanded state of a collapsible block. |
open | boolean | — | Controlled expanded state; use v-model:open. |
expandLabel | string | Expand code | Text and accessible name of the expand control. |
collapseLabel | string | Collapse code | Text and accessible name of the collapse control. |
labels | CodeBlockLabels | provider | Overrides 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
| Export | Description |
|---|---|
CodeBlock | Compound API exposing Root and Provider. |
CodeBlockRoot | Unaugmented code display component. |
ShikiProvider | Lazy-loads one shared Shiki highlighter for all child blocks. |
useShikiHighlighter | Access highlight, loading state, and labels inside a provider. |
normalizeCodeBlockLanguage | Resolves aliases to supported language ids. |
DEFAULT_CODE_BLOCK_LABELS | Default copy-button and language-selector labels. |
CodeBlockLabels | Copy-button and language-selector label overrides. |
CodeBlockEmits | Events, including update:lang for language selection. |
CodeBlockLanguageOption | A language choice with a label and value. |
CodeBlockProps | Public component props. |
LanguageAlias | Supported shorthand language ids. |
LanguageInput | Supported language ids and aliases. |
ShikiEngine | Supported Shiki engine names. |
ShikiProviderProps | Provider props. |
SupportedLanguage | Languages available through the dedicated entry point. |
UseShikiHighlighterResult | Return type of useShikiHighlighter. |