Skip to content
edmi-ui

Code Block

Card with mono code, optional line highlight and a copy button.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Claude Code
claude mcp add stockbreak \  https://stockbreak.fun/api/mcp
Claude Code
claude mcp add stockbreak \  https://stockbreak.fun/api/mcp
Claude Code
claude mcp add stockbreak \  https://stockbreak.fun/api/mcp
Terminal window
npx shadcn@latest add @edmi-ui/code-block
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/code-block
Terminal window
yarn dlx shadcn@latest add @edmi-ui/code-block
Terminal window
bunx shadcn@latest add @edmi-ui/code-block

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/code-block
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/code-block
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/code-block
Terminal window
bunx shadcn-vue@latest add @edmi-ui/code-block

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/code-block.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/code-block.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/code-block.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/code-block.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import { CodeBlock } from "@/components/code-block/code-block";
<CodeBlock
className="w-[420px] max-w-full"
title="Claude Code"
code={"claude mcp add stockbreak \\\n https://stockbreak.fun/api/mcp"}
highlightLines={[2]}
/>
<script setup lang="ts">
import { CodeBlock } from "@/components/ui/code-block";
</script>
<template>
<CodeBlock class="w-[420px] max-w-full" title="Claude Code" :highlight-lines="[2]" />
</template>
<script lang="ts">
import { CodeBlock } from "$lib/components/ui/code-block";
</script>
<CodeBlock class="w-[420px] max-w-full" title="Claude Code" highlightLines={[2]} />
registry/blocks/code-block/code-block.tsx
"use client";
import { cn } from "cn";
import * as React from "react";
import { CheckIcon, CopyIcon } from "@phosphor-icons/react";
import { Button } from "@/registry/edmi/ui/button";
import { Card } from "@/registry/edmi/ui/card";
import type { Elevation } from "@/registry/edmi/ui/elevation";
type CodeBlockProps = Omit<React.ComponentProps<typeof Card>, "title"> & {
/** Source text. */
code: string;
/** Header label (file name or tool). */
title?: React.ReactNode;
/** 1-based line numbers to highlight. */
highlightLines?: number[];
/** Show the copy button (default true). */
copyable?: boolean;
/** Called after a successful copy. */
onCopy?: (code: string) => void;
/** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2. */
elevation?: Elevation;
};
function CodeBlock({
className,
code,
title,
highlightLines,
copyable = true,
onCopy,
elevation,
...props
}: CodeBlockProps) {
const [copied, setCopied] = React.useState(false);
const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(
undefined,
);
React.useEffect(() => () => clearTimeout(timer.current), []);
async function copy() {
try {
await navigator.clipboard.writeText(code);
} catch {
return;
}
onCopy?.(code);
setCopied(true);
clearTimeout(timer.current);
timer.current = setTimeout(() => setCopied(false), 1500);
}
const lines = code.split("\n");
return (
<Card
data-slot="code-block"
elevation={elevation}
className={cn("gap-0 overflow-hidden p-0", className)}
{...props}
>
{title || copyable ? (
<div className="flex items-center justify-between border-b border-border bg-muted py-2 pr-2 pl-3.5">
<span className="font-mono text-[11.5px] text-muted-foreground">
{title}
</span>
{copyable ? (
<Button
variant="ghost"
size="icon-xs"
aria-label={copied ? "Copied" : "Copy code"}
onClick={copy}
>
{copied ? (
<CheckIcon
/>
) : (
<CopyIcon
/>
)}
</Button>
) : null}
</div>
) : null}
<pre className="m-0 overflow-x-auto bg-card py-3.5 font-mono text-[12.5px] leading-[1.65] whitespace-pre-wrap text-foreground-2">
<code>
{lines.map((line, i) => (
<span
// biome-ignore lint/suspicious/noArrayIndexKey: static lines
key={i}
data-highlighted={
highlightLines?.includes(i + 1) ? "" : undefined
}
className="block border-l-2 border-transparent px-3.5 data-[highlighted]:border-brand data-[highlighted]:bg-accent"
>
{line || "​"}
</span>
))}
</code>
</pre>
</Card>
);
}
export { CodeBlock };
registry/ui/code-block/CodeBlock.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { computed, onBeforeUnmount, ref } from 'vue'
import { Check, Copy } from '@lucide/vue'
import { Button } from '@/registry/edmi/ui/button'
import { Card } from '@/registry/edmi/ui/card'
import type { Elevation } from '@/registry/edmi/ui/elevation'
import { cn } from '@/registry/edmi/lib/utils'
const props = withDefaults(defineProps<{
/** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2 */
elevation?: Elevation
class?: HTMLAttributes['class']
/** Source text. */
code: string
/** Header label (file name or tool); slot `title` for rich content. */
title?: string
/** 1-based line numbers to highlight. */
highlightLines?: number[]
/** Show the copy button (default true). */
copyable?: boolean
}>(), { copyable: true })
const emit = defineEmits<{ copy: [code: string] }>()
const copied = ref(false)
let timer: ReturnType<typeof setTimeout> | undefined
onBeforeUnmount(() => clearTimeout(timer))
async function copy() {
try {
await navigator.clipboard.writeText(props.code)
} catch {
return
}
emit('copy', props.code)
copied.value = true
clearTimeout(timer)
timer = setTimeout(() => { copied.value = false }, 1500)
}
const lines = computed(() => props.code.split('\n'))
</script>
<template>
<Card :elevation="elevation" data-slot="code-block" :class="cn('gap-0 overflow-hidden p-0', props.class)">
<div
v-if="title || $slots.title || copyable"
class="flex items-center justify-between border-b border-border bg-muted py-2 pr-2 pl-3.5"
>
<span class="font-mono text-[11.5px] text-muted-foreground"><slot name="title">{{ title }}</slot></span>
<Button
v-if="copyable"
variant="ghost"
size="icon-xs"
:aria-label="copied ? 'Copied' : 'Copy code'"
@click="copy"
>
<Check v-if="copied" />
<Copy v-else />
</Button>
</div>
<pre class="m-0 overflow-x-auto bg-card py-3.5 font-mono text-[12.5px] leading-[1.65] whitespace-pre-wrap text-foreground-2"><code><span
v-for="(line, i) in lines"
:key="i"
:data-highlighted="highlightLines?.includes(i + 1) ? '' : undefined"
class="block border-l-2 border-transparent px-3.5 data-[highlighted]:border-brand data-[highlighted]:bg-accent"
>{{ line || '​' }}</span></code></pre>
</Card>
</template>
registry/ui/code-block/index.ts
export { default as CodeBlock } from "./CodeBlock.vue";
src/lib/registry/ui/code-block/code-block.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import { Button } from "$lib/registry/ui/button/index.js";
import { Card } from "$lib/registry/ui/card/index.js";
import type { Elevation } from "$lib/registry/ui/elevation/index.js";
import CheckIcon from "phosphor-svelte/lib/Check";
import CopyIcon from "phosphor-svelte/lib/Copy";
import type { ComponentProps, Snippet } from "svelte";
let {
ref = $bindable(null),
class: className,
code,
title,
highlightLines,
copyable = true,
onCopy,
elevation = "auto",
...restProps
}: Omit<ComponentProps<typeof Card>, "title" | "children"> & {
/** Source text. */
code: string;
/** Header label (file name or tool), or a snippet for rich content. */
title?: string | Snippet;
/** 1-based line numbers to highlight. */
highlightLines?: number[];
/** Show the copy button (default true). */
copyable?: boolean;
/** Called after a successful copy. */
onCopy?: (code: string) => void;
/** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2. */
elevation?: Elevation;
} = $props();
let copied = $state(false);
let timer: ReturnType<typeof setTimeout> | undefined;
$effect(() => () => clearTimeout(timer));
async function copy() {
try {
await navigator.clipboard.writeText(code);
} catch {
return;
}
onCopy?.(code);
copied = true;
clearTimeout(timer);
timer = setTimeout(() => (copied = false), 1500);
}
const lines = $derived(code.split("\n"));
</script>
<Card bind:ref {elevation} data-slot="code-block" class={cn("gap-0 overflow-hidden p-0", className)} {...restProps}>
{#if title || copyable}
<div class="flex items-center justify-between border-b border-border bg-muted py-2 pr-2 pl-3.5">
<span class="font-mono text-[11.5px] text-muted-foreground">{#if typeof title === "function"}{@render title()}{:else}{title}{/if}</span>
{#if copyable}
<Button variant="ghost" size="icon-xs" aria-label={copied ? "Copied" : "Copy code"} onclick={copy}>
{#if copied}
<CheckIcon
/>
{:else}
<CopyIcon
/>
{/if}
</Button>
{/if}
</div>
{/if}
<pre
class="m-0 overflow-x-auto bg-card py-3.5 font-mono text-[12.5px] leading-[1.65] whitespace-pre-wrap text-foreground-2"><code
>{#each lines as line, i (i)}<span
data-highlighted={highlightLines?.includes(i + 1) ? "" : undefined}
class="block border-l-2 border-transparent px-3.5 data-[highlighted]:border-brand data-[highlighted]:bg-accent"
>{line || "​"}</span
>{/each}</code
></pre>
</Card>
src/lib/registry/ui/code-block/index.ts
import Root from "./code-block.svelte";
export {
Root,
//
Root as CodeBlock,
};

See the Elevation guide for the levels, roles and layered mode.

Flat by default. elevation is forwarded to the card: sunken (-1) a soft inset well, raised (+1) the bevel, floating (+2) the bevel plus one soft drop. In a layered ElevationProvider the card resolves to its role default (raised).

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Flat (0)

Claude Code
claude mcp add stockbreak \  https://stockbreak.fun/api/mcp

Raised (+1)

Claude Code
claude mcp add stockbreak \  https://stockbreak.fun/api/mcp

Floating (+2)

Claude Code
claude mcp add stockbreak \  https://stockbreak.fun/api/mcp

Flat (0)

Claude Code
claude mcp add stockbreak \  https://stockbreak.fun/api/mcp

Raised (+1)

Claude Code
claude mcp add stockbreak \  https://stockbreak.fun/api/mcp

Floating (+2)

Claude Code
claude mcp add stockbreak \  https://stockbreak.fun/api/mcp

Flat (0)

Claude Code
claude mcp add stockbreak \  https://stockbreak.fun/api/mcp

Raised (+1)

Claude Code
claude mcp add stockbreak \  https://stockbreak.fun/api/mcp

Floating (+2)

Claude Code
claude mcp add stockbreak \  https://stockbreak.fun/api/mcp
Part Prop Type Default
CodeBlock code string required
title React ReactNode; Vue string or slot title; Svelte string | Snippet none
highlightLines number[] (1-based) none
copyable boolean true
onCopy React onCopy(code); Vue @copy; Svelte onCopy(code). Called after a successful copy none
elevation ✦ "auto" | "sunken" | "flat" | "raised" | "floating" "auto"

Plain mono pre (no syntax highlighting, no extra dependency). The copy button shows a check for 1.5s.

code-block is a small pattern: a card with plain mono text, a line highlight and a copy button, no dependencies. It suits docs and landing snippets. ai-code-block is the full AI pack component (Shiki syntax highlighting tied to the chart tokens, language selector, composable parts). The two are separate on purpose; pick ai-code-block when you need real highlighting.

  • Whole block is an Edmi addition (DESIGN §5 Patterns, board 12). Composed from: button, card.
  • Install: shadcn add @edmi-ui/code-block.