No highlighter registered in this preview — plain text + chrome only.
pnpm install
pnpm dev
export function add(a: number, b: number): number {
return a + b;
}
function greet(name) {
return `Hello, ${name}!`;
}
console.log(greet("Malevich"));
.button {
background: var(--color-accent);
color: var(--color-ink-inverse);
}
# Hello
A paragraph with *emphasis* and **bold**.
Just plain text, no copy button.
/*
* Code-block — block
*
* Multi-line code surface with optional syntax highlighting (via
* registerHighlighter), language label, line numbers, and copy
* button (via the copyable behavior modifier, default-on per the
* v1.0 master diff §4.8).
*
* Per the answered design question:
* highlight(code, lang) → string HTML (adapter pattern, like Icon)
*
* <pre class="code-block" data-lang="ts" data-copyable="true">
* <code>const x = 1;</code>
* </pre>
*
* <pre class="code-block" data-lang="bash" data-line-numbers="true">
* <code>pnpm install
* pnpm dev</code>
* </pre>
*/
@layer malevich.components {
.code-block {
--code-block-counter: 1;
position: relative;
display: block;
margin: 0;
padding-block: var(--space-inset-block-m);
padding-inline: var(--space-inset-block-m);
/* Reserve room for the copy button (per .malevich-copyable__button position) */
padding-inline-end: calc(var(--space-inset-block-m) + var(--size-control-s));
background-color: var(--color-surface-canvas);
color: var(--color-ink-strong);
border: var(--border-width-hairline) solid var(--color-border-muted);
border-radius: var(--radius-card);
overflow-x: auto;
}
/* The <code> child gets the mono typography */
.code-block > code,
.code-block code {
display: block;
font-family: var(--font-mono-m-family);
font-size: var(--font-mono-m-size);
font-weight: var(--font-mono-m-weight);
line-height: var(--font-mono-m-line-height);
letter-spacing: var(--font-mono-m-letter-spacing);
color: inherit;
background: transparent;
padding: 0;
border: 0;
white-space: pre;
}
/* ---- Language label ---- */
.code-block[data-lang]::before {
content: attr(data-lang);
position: absolute;
inset-block-start: var(--space-inset-element-s);
inset-inline-start: var(--space-inset-element-s);
color: var(--color-ink-subtle);
font-family: var(--font-overline-family);
font-size: var(--font-overline-size);
font-weight: var(--font-overline-weight);
line-height: var(--font-overline-line-height);
letter-spacing: var(--font-overline-letter-spacing);
text-transform: uppercase;
user-select: none;
pointer-events: none;
}
/* Push code body down to make room for the language label */
.code-block[data-lang] {
padding-block-start: calc(var(--space-inset-block-m) + var(--space-inset-element-m));
}
/* ---- Line numbers via CSS counter ----
*
* Authors break code into one <span class="code-block__line"> per
* line OR rely on the runtime to split textContent. Without the
* runtime, line-numbers fall back to a single counter on the
* <code> element (the whole block reads as line 1).
*/
.code-block[data-line-numbers="true"] > code,
.code-block[data-line-numbers="true"] code {
counter-reset: code-block-line;
}
.code-block[data-line-numbers="true"] .code-block__line {
display: block;
counter-increment: code-block-line;
padding-inline-start: var(--space-inset-section-s);
position: relative;
}
.code-block[data-line-numbers="true"] .code-block__line::before {
content: counter(code-block-line);
position: absolute;
inset-inline-start: 0;
color: var(--color-ink-subtle);
user-select: none;
text-align: end;
min-inline-size: var(--space-inset-element-l);
font-variant-numeric: tabular-nums;
}
/* ---- Variants ---- */
.code-block.-flat {
border: 0;
background-color: var(--color-surface-raised);
}
.code-block.-on-inverse {
background-color: var(--color-ink-strong);
color: var(--color-ink-inverse);
border-color: var(--color-ink-strong);
}
}
// Code-block — adapter + runtime.
//
// Per the answered design question:
// highlight(code, lang) => string (adapter pattern, like Icon)
//
// Consumers register a highlighter:
//
// import { registerHighlighter } from "@malevich/components";
// import { highlight } from "@malevich/code-block-shiki"; // example
// registerHighlighter(highlight);
//
// The runtime walks `.code-block[data-lang]` elements, calls the
// registered highlighter with the text content + language, and swaps
// the <code> innerHTML. If no highlighter is registered, the block
// renders plain text. If data-line-numbers="true" is set, the runtime
// wraps each line in a `<span class="code-block__line">`.
const READY_ATTR = "data-malevich-ready";
export type Highlighter = (code: string, lang: string) => string;
let registeredHighlighter: Highlighter | null = null;
/**
* Register a syntax highlighter. The function receives raw code and
* a language identifier (the value of `data-lang`), and must return
* inner HTML to be placed inside the host's <code> element. Return
* an empty string or the original code to opt out for a specific
* call.
*/
export function registerHighlighter(highlighter: Highlighter): void {
registeredHighlighter = highlighter;
}
export function unregisterHighlighter(): void {
registeredHighlighter = null;
}
function wrapLines(code: HTMLElement): void {
// Split innerHTML on \n into spans. Preserves any inner tags by
// splitting at top-level only.
const html = code.innerHTML;
if (!html.includes("\n")) {
code.innerHTML = `<span class="code-block__line">${html}</span>`;
return;
}
const lines = html.split("\n");
code.innerHTML = lines
.map((l) => `<span class="code-block__line">${l}</span>`)
.join("");
}
export function initCodeBlock(host: HTMLElement): void {
if (host.getAttribute(READY_ATTR) === "true") return;
const code = host.querySelector<HTMLElement>("code");
if (!code) {
host.setAttribute(READY_ATTR, "true");
return;
}
const lang = host.getAttribute("data-lang") ?? "";
const wantsLineNumbers = host.getAttribute("data-line-numbers") === "true";
// Run the highlighter, if any.
if (registeredHighlighter && lang) {
const raw = code.textContent ?? "";
try {
const highlighted = registeredHighlighter(raw, lang);
if (highlighted && highlighted.length > 0) {
code.innerHTML = highlighted;
}
} catch {
// Silent fallback to plain text.
}
}
if (wantsLineNumbers) wrapLines(code);
host.setAttribute(READY_ATTR, "true");
}
export function initCodeBlocks(root: ParentNode = document): void {
for (const host of root.querySelectorAll<HTMLElement>(".code-block")) {
initCodeBlock(host);
}
}
/**
* Re-run highlighting + line-wrapping on every existing code-block.
* Useful after registering a highlighter at runtime.
*/
export function refreshCodeBlocks(root: ParentNode = document): void {
for (const host of root.querySelectorAll<HTMLElement>(".code-block")) {
host.removeAttribute(READY_ATTR);
initCodeBlock(host);
}
}
# Code-Block — AGENTS.md
> Auto-generated from `code-block.docs.md` + the modifier applicability matrix.
> Edit those source files; this file regenerates on build.
## Identity
- **Component:** `code-block`
- **Layer:** blocks
- **Status:** stable
- **Last updated:** 2026-05-19
## Summary
- Documentation blocks longer than a single line.
## Variants
| Variant | Class | Effect |
|---|---|---|
| Default | `.code-block` | Bordered card on canvas |
| Flat | `.code-block.-flat` | No border, raised background |
| Inverse | `.code-block.-on-inverse` | Dark surface |
## Modifier applicability
Per ADR-0007. Modifiers attach via `data-{category}="value"` attributes.
| Category | Accepts |
|---|---|
| `background` | — |
| `surface` | `flat`, `elevated` |
| `effect` | `ring` |
| `shader` | — |
| `rhythm` | `compact`, `regular` |
| `motion` | — |
| `behavior` | `copyable` |
## Anatomy
```html
<!-- Plain block, no highlighter registered: renders as plain text + copy -->
<pre class="code-block" data-lang="ts">
<code>const x = 1;
const y = 2;
const z = x + y;</code>
</pre>
<!-- With language label, line numbers, copy on hover -->
<pre class="code-block" data-lang="bash" data-line-numbers="true">
<code>pnpm install
pnpm dev</code>
</pre>
<!-- Variants -->
<pre class="code-block -flat" data-lang="css">…</pre>
<pre class="code-block -on-inverse" data-lang="md">…</pre>
```
## Tokens consumed
- `--color-surface-canvas` / `--color-surface-raised` / `--color-ink-strong`
- `--color-ink-subtle` — language label + line-number color
- `--color-ink-inverse` — inverse text
- `--color-border-muted` — border
- `--space-inset-block-m`, `--space-inset-element-{s,m,l}`,
`--space-inset-section-s`
- `--size-control-s` — reserved space for copy button
- `--radius-card`
- `--border-width-hairline`
- `--font-mono-m-*`, `--font-overline-*`
## Accessibility contract
- Use `<pre>` as the host and a child `<code>` for the content. Both
carry implicit semantics.
- The language label is a decorative `::before` pseudo-element —
excluded from the accessibility tree.
- Line numbers are decorative `::before` counters — excluded.
- The copy button has `aria-label="Copy to clipboard"` (from the
copyable behavior).
## Guidance
### Do
- Use `<pre><code>` markup.
- Set `data-lang` so the language label renders even without a
highlighter.
- Reach for `refreshCodeBlocks()` after registering or swapping a
highlighter at runtime.
### Don't
- Don't put inline tokens inside the host `<pre>` — keep code inside
the inner `<code>`.
- Don't write highlighting HTML manually for static blocks. Register
a highlighter or omit highlighting.