Skip to content
ShineUI
Install

CodeBlock

Code that reads like code.

<sui-code-block>MiscStable

About

Code pane with syntax color from Shiki, loaded on first render. Until then, or if it fails, plain mono at the same metrics stands in. Use it for snippets a reader will copy. It is dark in either site mode unless data-sui-code="light". Tokens are classed elements, not HTML or style attributes, so a strict CSP needs nothing extra.

Usage

<sui-code-block language="jsx" label="mount.jsx"></sui-code-block> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-code-block"); el.lines = [ "// one accent action per view", "const { Button } = window.SB;", "const root = ReactDOM.createRoot(el);", "root.render(<Button variant='accent' label='Pay bills' />);", ]; /* string[] | undefined */ el.addEventListener("suiCopy", (e) => console.log(e.detail)); </script>
import { SuiCodeBlock } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const lines = [ "// one accent action per view", "const { Button } = window.SB;", "const root = ReactDOM.createRoot(el);", "root.render(<Button variant='accent' label='Pay bills' />);", ]; /* string[] | undefined */ return <SuiCodeBlock language="jsx" label="mount.jsx" lines={lines} onSuiCopy={(e) => console.log(e.detail)} />; }
<script setup> import { SuiCodeBlock } from "@shineui/vue"; import "@shineui/core/shineui.css"; const lines = [ "// one accent action per view", "const { Button } = window.SB;", "const root = ReactDOM.createRoot(el);", "root.render(<Button variant='accent' label='Pay bills' />);", ]; // string[] | undefined const onChange = (e) => console.log(e.detail); </script> <template> <SuiCodeBlock language="jsx" label="mount.jsx" :lines="lines" @sui-copy="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiCodeBlock } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiCodeBlock], template: `<sui-code-block language="jsx" label="mount.jsx" [lines]="lines" (suiCopy)="onChange($event)"></sui-code-block>`, }) export class Example { lines = [ "// one accent action per view", "const { Button } = window.SB;", "const root = ReactDOM.createRoot(el);", "root.render(<Button variant='accent' label='Pay bills' />);", ]; // string[] | undefined onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

Shiki loads on first render; plain mono at the same metrics shows until it does, so the pane works offline. The first highlight fetches about 151 KB gzip and costs about 90ms of main-thread work for 800 lines. The Copy tool is named "Copy to clipboard", and a polite live region announces "Copied" on a confirmed write.

<div style="max-width:640px"><sui-code-block language="jsx" label="mount.jsx"></sui-code-block></div>
// setup(root): properties and events setup(root) { root.querySelector('sui-code-block').lines = mountJsx; }

Grammars

jsx, tsx, js, ts, html, css, json and bash. Any other value, text for instance, renders plain mono text and fetches no grammar at all, which is the opt-out for a page that shows one snippet. Without a label the tools float in the top-right corner instead of sitting in a header bar.

<div style="display:grid;gap:14px;max-width:640px"> <sui-code-block id="cb-json" language="json" label="schedule.json"></sui-code-block> <sui-code-block id="cb-bash" language="bash"></sui-code-block> </div>
// setup(root): properties and events setup(root) { root.querySelector('#cb-json').lines = scheduleJson; root.querySelector('#cb-bash').lines = deployBash; }

Wrap and height

wrapToggle adds a Wrap switch beside Copy, reporting its state through aria-pressed. maxHeight caps the scroll region; both tools sit outside it and stay put while the code scrolls.

<div style="display:grid;gap:14px;max-width:640px"> <sui-code-block id="cb-wrap" language="ts" label="post-invoice.ts" wrap-toggle></sui-code-block> <sui-code-block id="cb-tall" language="bash" label="release.sh" max-height="96"></sui-code-block> </div>
// setup(root): properties and events setup(root) { root.querySelector('#cb-wrap').lines = [ 'export async function postInvoice(id: string, period: string, memo: string, approver: string, postedAt: Date) {', ' return api.post(`/invoices/${id}/post`, { period, memo, approver, postedAt });', '}', ]; root.querySelector('#cb-tall').lines = [...deployBash, ...deployBash]; }

Size and no copy tool

sm is 11px mono for a side panel, md is 12px. no-copy hides the Copy tool where the snippet is illustrative and should not be pasted anywhere.

<div style="display:grid;gap:14px;max-width:640px"> <sui-code-block id="cb-sm" language="json" size="sm" no-copy></sui-code-block> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#cb-sm'); el.lines = scheduleJson; }

Events

suiCopy fires after the source reaches the clipboard and carries the copied text. A refused write shows no Copied state and emits suiCopyError with the text that did not make it, which is where your own toast belongs.

Press Copy.
<div style="display:grid;gap:10px;max-width:640px"> <sui-code-block id="cb-events" language="jsx" label="mount.jsx"></sui-code-block> <span id="cb-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Press Copy.</span> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#cb-events'); const log = root.querySelector('#cb-log'); el.lines = mountJsx; el.addEventListener('suiCopy', (e) => { log.textContent = `Copied ${e.detail.split('\n').length} line(s).`; }); el.addEventListener('suiCopyError', () => { log.textContent = 'The clipboard refused the write. Nothing was copied.'; }); }

Keyboard

KeyAction
TabMoves through the wrap toggle and the copy control when they are shown. The pane itself is a scrollable group that the element does not make a tab stop; a browser that focuses scrollers on its own reaches it after the controls.
Enter SpaceOn the wrap toggle, wraps or unwraps long lines; the button reads aria-pressed. On the copy control, copies the code, announces Copied and emits suiCopy, or suiCopyError when the clipboard is blocked.

API

<sui-code-block>

Properties

PropertyAttributeTypeDefaultDescription
codecodestringstring | undefinedSource text. Takes precedence over lines.
labellabelstringstring | undefinedFilename or title. Renders a header bar with a language chip and the tools; without it the tools float in the top-right corner.
languagelanguageSuiCodeLang"bash" | "css" | "html" | "js" | "json" | "jsx" | "ts" | "tsx"'jsx'Grammar: jsx (default), tsx, js, ts, html, css, json or bash. Any other value, text for instance, renders the source as plain mono text and loads no highlighter at all: that is the way to show a snippet on a page that should not fetch the grammar bundle, which is about 151 KB gzip on first use. The reference called this lang; that name is the native content-language attribute, so it is language here.
linesproperty onlystring[]string[] | undefinedAlternative to code: an array of lines, joined with newlines. Property, not attribute.
maxHeightmax-heightnumbernumber | undefinedScroll region height cap in px. The tools sit outside the region and stay put while the code scrolls.
noCopyno-copybooleanfalseHides the Copy tool, which shows by default. The tool is named "Copy to clipboard" and a polite live region announces "Copied" after a confirmed write.
sizesize"md" | "sm"'md'Mono size: sm 11px, md 12px. Reflected.
wrapwrapbooleanfalseSoft-wrap long lines. Mutable and reflected: the Wrap tool toggles it.
wrapTogglewrap-togglebooleanfalseShows a Wrap toggle beside Copy. It reports its state through aria-pressed.

Events

EventDetailReactVueDescription
suiCopystringonSuiCopy@sui-copyFires after the source reaches the clipboard; detail is the copied text. Copy falls back to a hidden textarea where navigator.clipboard is absent, so plain http still works.
suiCopyErrorstringonSuiCopyError@sui-copy-errorFires when the clipboard refused the write. No Copied state shows; detail is the text that was not copied, which is where your own toast belongs.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves focus to the pane's first tab stop: the Wrap or Copy tool, else the code itself when it scrolls. A pane with no tools that fits its box has no tab stop and keeps focus where it is. The host's own focus() does the same.

Types

SuiCodeLang typeGrammars the code pane accepts.
No members.
SuiCodeLang typeLanguages the code pane accepts (the reference contract).
No members.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also