Code Block
Card with mono code, optional line highlight and a copy button.
import { CodeBlock } from "@/components/code-block/code-block";
export default function Demo() { return ( <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";
const code = "claude mcp add stockbreak \\\n https://stockbreak.fun/api/mcp";</script>
<template> <CodeBlock class="w-[420px] max-w-full" title="Claude Code" :code="code" :highlight-lines="[2]" /></template><script lang="ts"> import { CodeBlock } from "$lib/components/ui/code-block";
const code = "claude mcp add stockbreak \\\n https://stockbreak.fun/api/mcp";</script>
<CodeBlock class="w-[420px] max-w-full" title="Claude Code" {code} highlightLines={[2]} />Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/code-blockpnpm dlx shadcn@latest add @edmi-ui/code-blockyarn dlx shadcn@latest add @edmi-ui/code-blockbunx shadcn@latest add @edmi-ui/code-blockPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
button,card,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/code-blockpnpm dlx shadcn-vue@latest add @edmi-ui/code-blockyarn dlx shadcn-vue@latest add @edmi-ui/code-blockbunx shadcn-vue@latest add @edmi-ui/code-blockPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @lucide/vueTerminal window pnpm add @lucide/vueTerminal window yarn add @lucide/vueTerminal window bun add @lucide/vueAdd the Edmi components this one builds on:
button,card,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/code-block.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/code-block.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/code-block.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/code-block.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
button,card,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
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]} />Component source
Section titled “Component source”"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 };<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> | undefinedonBeforeUnmount(() => 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>export { default as CodeBlock } from "./CodeBlock.vue";<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>import Root from "./code-block.svelte";
export { Root, // Root as CodeBlock,};Elevation ✦
Section titled “Elevation ✦”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).
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
import { CodeBlock } from "@/components/code-block/code-block";import type { Elevation } from "@/components/ui/elevation";
function Sample({ elevation }: { elevation: Elevation }) { return ( <CodeBlock elevation={elevation} className="w-[420px] max-w-full" title="Claude Code" code={"claude mcp add stockbreak \\\n https://stockbreak.fun/api/mcp"} highlightLines={[2]} /> );}
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="flex flex-col gap-6"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <Sample elevation={value} /> </div> ))} </div> );}<script setup lang="ts">import { CodeBlock } from "@/components/ui/code-block";
const code = "claude mcp add stockbreak \\\n https://stockbreak.fun/api/mcp";
const levels = [ { value: "flat", label: "Flat (0)" } as const, { value: "raised", label: "Raised (+1)" } as const, { value: "floating", label: "Floating (+2)" } as const,];</script>
<template> <div class="flex flex-col gap-6"> <div v-for="l in levels" :key="l.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ l.label }}</p> <CodeBlock :elevation="l.value" class="w-[420px] max-w-full" title="Claude Code" :code="code" :highlight-lines="[2]" /> </div> </div></template><script lang="ts"> import { CodeBlock } from "$lib/components/ui/code-block";
const code = "claude mcp add stockbreak \\\n https://stockbreak.fun/api/mcp";
const levels = [ { value: "flat", label: "Flat (0)" } as const, { value: "raised", label: "Raised (+1)" } as const, { value: "floating", label: "Floating (+2)" } as const, ];</script>
<div class="flex flex-col gap-6"> {#each levels as l (l.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{l.label}</p><CodeBlock elevation={l.value} class="w-[420px] max-w-full" title="Claude Code" {code} highlightLines={[2]} /> </div> {/each}</div>| 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 vs AI code block
Section titled “Code block vs AI code block”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.
✦ Edmi additions
Section titled “✦ Edmi additions”- Whole block is an Edmi addition (DESIGN §5 Patterns, board 12). Composed from: button, card.
- Install:
shadcn add @edmi-ui/code-block.