AI output
Code block
Code inside an AI answer that is highlighted while it streams in behind a caret, where wrapping and folding ease the block to its new height and Copy and Apply confirm in place with a drawn check and a morphing label.
Live preview
Loading preview
A TypeScript file for a small invoice helper streams in line by line with a caret, then shows Copy, wrap and Apply; a second, longer Python script arrives complete and starts folded with "Show 24 more lines". Replay streams the first block again and folds the second.
Installation
Run this in your app after the one-time setup on the Getting started page.
$ npx @ganisigit/lunia-ds add code-blockUsage
The demo above, as code.
'use client';
import * as React from 'react';
import { type ApplyStatus, CodeBlock } from './code-block';
const INVOICE = `export function formatInvoice(total: number) {
const tax = total * 0.11;
// round to two decimals
return \`Total: \${(total + tax).toFixed(2)}\`;
}`;
const REPORT = `import csv
from pathlib import Path
SOURCE = Path("orders.csv")
REPORT = Path("report.txt")
def read_orders(path):
with path.open() as file:
return list(csv.DictReader(file))
def total_by_city(orders):
totals = {}
for order in orders:
city = order["city"]
amount = float(order["amount"])
totals[city] = totals.get(city, 0) + amount
return totals
def top_cities(totals, count=5):
pairs = sorted(totals.items(), key=lambda p: p[1], reverse=True)
return pairs[:count]
def write_report(rows, path):
lines = ["City, Total"]
for city, total in rows:
lines.append(f"{city}, {total:.2f}")
lines.append("")
path.write_text("\\n".join(lines))
def main():
orders = read_orders(SOURCE)
totals = total_by_city(orders)
write_report(top_cities(totals), REPORT)
print("Report written")
# Run the report when the file is started.
if __name__ == "__main__":
main()`;
const CHUNK = 5;
const TICK = 35;
export default function CodeBlockDemo({ replay = 0 }: { replay?: number }) {
const [shown, setShown] = React.useState(INVOICE.length);
const [apply, setApply] = React.useState<ApplyStatus>('idle');
const timer = React.useRef(0);
// Replay streams the first block in again, and mounts the second one again so that it starts folded.
React.useEffect(() => {
if (replay === 0) return;
setApply('idle');
setShown(0);
timer.current = window.setInterval(() => {
setShown((n) => {
if (n + CHUNK >= INVOICE.length) {
window.clearInterval(timer.current);
return INVOICE.length;
}
return n + CHUNK;
});
}, TICK);
return () => window.clearInterval(timer.current);
}, [replay]);
const streaming = shown < INVOICE.length;
return (
<div className="flex w-full max-w-[560px] flex-col gap-6">
<CodeBlock
code={INVOICE.slice(0, shown)}
language="ts"
filename="invoice.ts"
streaming={streaming}
applyStatus={apply}
onApply={() => {
setApply('loading');
window.setTimeout(() => setApply('success'), 1100);
}}
/>
<CodeBlock key={replay} code={REPORT} language="python" filename="report.py" />
</div>
);
}
API
Parts: root (card), header: file name and language, wrap toggle, copy button (status button), apply button (status button, optional), code body: line numbers (sticky), highlighted lines, caret while streaming, fold fade (over folded code), fold bar: show or hide button with chevron.
| Prop | Type | Default | Description |
|---|---|---|---|
| code | string | - | The code so far. Required. Update it as the stream delivers more. |
| language | string | undefined | Language id such as "ts", "python" or "bash"; shown in the header and used for comment rules. |
| filename | string | undefined | File name shown in the header before the language. |
| streaming | boolean | false | True while the code is still arriving: shows the caret, never folds and waits to enable Apply. |
| lineNumbers | boolean | true | Shows line numbers. |
| collapseAfter | number | 18 | Folds code longer than this many lines plus 3; 0 never folds. Code you watched stream in stays open. |
| highlight | boolean | true | Turns the built-in light highlighter on or off. |
| applyStatus | "idle" | "loading" | "success" | "error" | "idle" | Status of the apply action, set by the parent after apply. New code resets success and error to idle. |
| applyLabel | string | "Apply" | Idle label of the Apply button. |
| Event | Description |
|---|---|
| copy | The code was copied, or copying failed. Payload: { ok: boolean }. |
| apply | The person pressed Apply or Try again; the parent sets applyStatus. The Apply button shows only when this has a listener. |
| expandedChange | Folded code was shown or hidden. Payload: the new state. |
Motion
Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.
| ID | Trigger | Element | Property | From → to | Timing | Reduced motion |
|---|---|---|---|---|---|---|
| CBK1 | While streaming | caret (after the last line) | opacity blink (steps), repeats | 1 → 0 | duration-6xl (900)ease-linear | No loop |
| CBK2 | Press Copy | copy button | status button motion (check draws, label "Copied"; "Could not copy" on error); back to Copy 1600ms after Copied, 2400ms after an error | Idle → Success or Error | duration-xl (380)ease-out | Swaps at once |
| CBK3 | Press Apply | apply button | status button motion ("Applying", check draws, "Applied"; "Try again" on error); after Applied it becomes the quiet ghost style | Idle → Success or Error | duration-xl (380)ease-out | Swaps at once |
| CBK4 | Streaming ends | apply button | opacity | 0.4 → 1 | duration-lg (320)ease-out | Swaps at once |
| CBK5 | Wrap toggled (code that does not fold) | code body | height | old height → new height | duration-3xl (460)ease-settle | Resizes at once |
| CBK6 | Show or hide folded lines | code body | max height | collapseAfter lines → full height | duration-3xl (460)ease-settle | Resizes at once |
| CBK7 | Show or hide folded lines | fold fade | opacity | 1 → 0 | duration-xl (380)ease-settle | Swaps at once |
| CBK8 | Show or hide folded lines | fold chevron | rotation | 0deg → 180deg | duration-lg (320)ease-settle | Swaps at once |
| CBK9 | Show or hide folded lines | fold button label | text morph | Show 24 more lines → Hide 24 lines | duration-3xl (460)ease-out | Swaps at once |
| CBK10 | Hover | wrap toggle, fold button | background color, text color | transparent, text-tertiary → bg-primary_hover, text-primary | duration-xs (160)ease-out | Swaps at once |
Accessibility
- The code is a pre element named by its language and file name, for example "ts, invoice.ts"; when lines do not wrap it is focusable (tabIndex 0) so keyboard users can scroll it sideways.
- Line numbers, the caret and the fold fade are aria-hidden; highlighting is color only and never carries meaning.
- The wrap toggle is a toggle button named "Wrap lines" with aria-pressed.
- The copy button is named "Copy code"; its status labels are read by the status button live region.
- The fold button has aria-expanded and aria-controls pointing to the code body, and says how many lines it shows or hides.
- Apply is disabled while streaming and says why with aria-describedby ("Available when the code is complete").
- Header buttons are 28px with a visible focus ring (Focus rings/focus-ring).