Code Block Since v4.1.0

Highlighted code with an optional header, copy button, line numbers and diff lines, for source, commands and output inside messages and traces.

Usage

Requires shiki, @shikijs/langs and @shikijs/themes.

Add provideHighlighter() to your application providers.

import { provideHighlighter } from '@/bui/ui/code-block';

export const appConfig: ApplicationConfig = {
providers: [
provideHighlighter(),
],
};
import {
BuiCodeBlock,
BuiCodeBlockActions,
BuiCodeBlockContent,
BuiCodeBlockCopyButton,
BuiCodeBlockFooter,
BuiCodeBlockHeader,
BuiCodeBlockIcon,
BuiCodeBlockLine,
BuiCodeBlockTitle,
} from '@/bui/ui/code-block';
<div buiCodeBlock>
<pre buiCodeBlockContent lang="typescript" [code]="code"></pre>
</div>

A text size class on buiCodeBlock, such as text-sm, sizes the whole block.

Example

buiCodeBlockHeader holds an optional icon, the title and the actions at the end.

Write a function that returns the overdue invoices.

This returns open invoices past their due date, oldest first:

overdue.ts
export function overdue(invoices: Invoice[], today: Date) {  return invoices    .filter((invoice) => invoice.status === 'open')    .filter((invoice) => invoice.dueDate < today)    .sort((a, b) => a.dueDate.getTime() - b.dueDate.getTime());}

Pass today's date in so the result is easy to test.

<div buiCodeBlock>
<div buiCodeBlockHeader>
<mat-icon buiCodeBlockIcon svgIcon="file-code" />
<span buiCodeBlockTitle>overdue.ts</span>
<div buiCodeBlockActions>
<button matIconButton buiCodeBlockCopyButton class="small" [value]="code">
<mat-icon svgIcon="copy" />
</button>
</div>
</div>
<pre buiCodeBlockContent lang="typescript" [code]="code"></pre>
</div>

Languages and themes

Each language loads the first time it is used, and code shows as plain text until then.

Catppuccin Latte and Macchiato are the light and dark themes. Pass langs to add languages or themes to replace both.

provideHighlighter({
langs: {
rust: () => import('@shikijs/langs/rust'),
},
themes: {
light: () => import('@shikijs/themes/github-light'),
dark: () => import('@shikijs/themes/github-dark'),
},
});

Angular, Bash, CSS, Diff, HTML, JavaScript, JSON, JSX, Markdown, Python, SCSS, SQL, TypeScript, TSX and YAML are included. Use ansi for terminal output with colors. Unknown languages and content without lang render as plain text.

Copy button

buiCodeBlockCopyButton goes on a small matIconButton and copies its value. It announces the copy and sets data-copied for two seconds, so projected icons can swap with group-data-copied/bui-code-block-copy-button: variants. Without a header, the actions float at the top end of the block, shown on hover or focus and always on touch screens.

Run the fee tests to confirm the change:

pnpm --filter billing test fees

All 12 pass, including the two new late fee cases.

<div buiCodeBlock>
<div buiCodeBlockActions>
<button matIconButton buiCodeBlockCopyButton class="small" [value]="command">
<mat-icon
svgIcon="copy"
class="group-data-copied/bui-code-block-copy-button:hidden"
/>
<mat-icon
svgIcon="copy-check"
class="hidden text-jade-11 group-data-copied/bui-code-block-copy-button:inline-block"
/>
</button>
</div>
<pre buiCodeBlockContent lang="bash" [code]="command"></pre>
</div>

Line numbers

lineNumbers adds a gutter as wide as the widest number, kept out of copied text. startLine sets the first number for an excerpt.

The filter lives in reminders.ts at line 11:

export function overdue(invoices: Invoice[], today: Date) {  return invoices    .filter((invoice) => invoice.status === 'open')    .filter((invoice) => invoice.dueDate < today)    .sort((a, b) => a.dueDate.getTime() - b.dueDate.getTime());}

Line 13 is the one that skips partially paid invoices.

<div buiCodeBlock>
<pre
buiCodeBlockContent
lang="typescript"
lineNumbers
[startLine]="11"
[code]="code"
></pre>
</div>

Diff lines

lineVariants maps displayed line numbers to added, removed or highlighted. Added and removed lines get a tint and a marker in the start padding.

Partially paid invoices were skipped. This includes them:

overdue.ts
export function overdue(invoices: Invoice[], today: Date) {  return invoices    .filter((invoice) => invoice.status === 'open')    .filter((invoice) => invoice.status !== 'paid')    .filter((invoice) => invoice.dueDate < today)    .sort((a, b) => a.dueDate.getTime() - b.dueDate.getTime());}

Partially paid invoices now count as open until the balance reaches zero.

<div buiCodeBlock>
<pre
buiCodeBlockContent
lang="typescript"
lineNumbers
[lineVariants]="{ 3: 'removed', 4: 'added' }"
[code]="code"
></pre>
</div>

Without code, the content takes hand authored buiCodeBlockLine rows, each with a variant.

<div buiCodeBlock>
<!-- A pre keeps whitespace, so lines sit back to back -->
<pre buiCodeBlockContent>@for (line of lines; track $index) {<span buiCodeBlockLine [variant]="line.variant">{{ line.text }}</span>}</pre>
</div>

Streaming

Bind the accumulated text to code. Finished lines keep their tokens and only the lines after them are tokenized again as the code grows. Disable the copy button until the code is complete.

send-reminders.ts
<div buiCodeBlock>
<div buiCodeBlockHeader>
<span buiCodeBlockTitle>send-reminders.ts</span>
<div buiCodeBlockActions>
<button
matIconButton
buiCodeBlockCopyButton
class="small"
[value]="code()"
[disabled]="streaming()"
>
<mat-icon svgIcon="copy" />
</button>
</div>
</div>
<pre buiCodeBlockContent lang="typescript" [code]="code()"></pre>
</div>

API Reference

buiCodeBlock

The frame. A text size class on it sizes the whole block.

buiCodeBlockHeader

Optional top row for the icon, title and actions.

buiCodeBlockIcon

Leading icon in the header, such as a file icon.

buiCodeBlockTitle

The file name or label. Truncates.

buiCodeBlockActions

Buttons at the end of the header, or floating at the top end without one.

buiCodeBlockCopyButton

Copies its value and announces it.

InputTypeDefaultDescription
valuestring Required The text to copy.

buiCodeBlockContent

Goes on a pre. Renders and highlights the code.

InputTypeDefaultDescription
codestring''The code to render.
langstring — The language to highlight with.
wrapbooleanfalseWraps long lines instead of scrolling horizontally.
lineNumbersbooleanfalseShows the line number gutter.
startLinenumber1The number of the first line.
lineVariantsRecord<number, 'added' | 'removed' | 'highlighted'>{}Variants by displayed line number.

buiCodeBlockLine

A hand authored line with a variant.

InputTypeDefaultDescription
variant'default' | 'added' | 'removed' | 'highlighted''default'The state of the line.

buiCodeBlockFooter

Optional bottom row, such as a show more button.