Playground
A real <textarea> layered over a highlighted <pre>. You get native editing (undo, IME, mobile keyboards, screen readers) with syntax colours from the theme tokens. It understands JavaScript, HTML and CSS, keeps indentation on Enter, indents and outdents selections with Tab and Shift+Tab, and emits sb-run on Ctrl/Cmd+Enter.
Examples
Initial code
Put the code in a child <script type="text/plain">. Its text is never parsed as HTML and never executed, and common indentation is removed.
<sb-code-editor language="html" label="index.html" style="inline-size: 100%">
<script type="text/plain">
<sb-button variant="pixel" caret>Blast off</sb-button>
<!-- edit me -->
</script>
</sb-code-editor>
Bound to a signal
value is a live property, so data-bind works (with __prop.value, and with the signal declared first).
<div data-signals:_css="'.planet { color: #8C6BFF; }'" style="display: grid; gap: 12px; inline-size: 100%">
<sb-code-editor language="css" data-bind:_css__prop.value></sb-code-editor>
<p data-text="$_css.length + ' characters'"></p>
</div>
Run with Ctrl+Enter
<div data-signals:_runs="0" style="display: grid; gap: 12px; inline-size: 100%">
<sb-code-editor language="js" data-on:sb-run="$_runs++"><script type="text/plain">console.log('press Ctrl+Enter')</script></sb-code-editor>
<p data-text="'Runs: ' + $_runs"></p>
</div>
Keyboard
| Keys | Action |
|---|---|
| Tab / Shift+Tab | Indent / outdent (the selected lines) |
| Enter | New line, keeping the indentation (plus one after {, ( or [) |
| Ctrl/Cmd+Enter | Emit sb-run |
| Esc, then Tab | Leave the editor, so Tab is never a focus trap |
With commands
Give it a name, and it emits sb-change with { name, value } when a value is committed: ready to post as a command. With confirm, it sets :state(pending) until the server's re-rendered attribute matches, and revert() goes back to the server's value when a command is rejected. See Commands and components and the Showcase.
Accessibility
The editable element is a native textarea with an accessible name (label, or "Code"). The highlighted layer and the gutter are aria-hidden.
Installation
Add Datastar with Rocket and the Starbase autoloader once per page, then use the tag. The autoloader imports each component the first time its tag appears, including tags added later by a Datastar morph.
<!-- Once per page: Datastar with Rocket, and the Starbase autoloader.
It loads every <sb-…> component the first time its tag appears. -->
<script type="importmap">
{ "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>
<!-- Optional, no flash of undefined elements: class="sb-cloak" on <html>, and -->
<style>.sb-cloak :not(:defined) { visibility: hidden }</style>
<sb-code-editor language="js" line-numbers style="--sb-code-editor-height: 7.5rem"><script type="text/plain">
rocket('sb-hello', {
render: ({ html }) => html`<b>hi</b>`,
})
</script></sb-code-editor>
<!-- In production, pin today's catalog instead of the latest: the browser then
refuses any file that changed. Add "integrity" to the import map above: the
hashes from https://starbase.zweiundeins.gmbh/c/@0a1ad1bf98a4/importmap.json and Datastar's, below. -->
<!--
<script type="importmap">
{ "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" },
"integrity": { "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js": "sha384-vUxZojLrF1Ar3de5h7VINqhJXBgjyZS4U49pHvGa87kum6j5Xn6JRziuARNw5ELG", "…": "…from importmap.json" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@0a1ad1bf98a4/autoloader.js" integrity="sha384-IvSCUjEWPS+wwXunU+48x96KmyWDx/Y4LeCwCL8cWFqJ/zHmTsTOsSMrzTOcpW3D"></script>
-->
<!-- Or load just this component, pinned to this version: -->
<!-- <script type="module" src="https://starbase.zweiundeins.gmbh/c/code-editor@ed9483f23494/code-editor.js" integrity="sha384-CfQNwv1hrAHzRIUDgzcTFa9T6ZTkvGvnOWp6B85CEUPlQn5TtYS8YfG1cGMxOVuI"></script> -->Size
Each file compressed on its own, the way it is served (gzip -9, brotli -11). Datastar and Rocket are shared by every component and not counted.
| File | Original | gzip | brotli |
|---|---|---|---|
code-editor.js | 11.4 kB | 4.4 kB | 3.8 kB |
vendor/prism.js | 23.5 kB | 8.8 kB | 7.9 kB |
| Total | 34.8 kB | 13.2 kB | 11.8 kB |
API reference
Props
| Attribute | Type | Default | Description |
|---|---|---|---|
language | "js" | "html" | "css" | "js" | Syntax to highlight. |
value | string | "" | The code (or a child <script type="text/plain">). A new value from the server replaces it; the live code is the value property. |
line-numbers | boolean | true | Show a line-number gutter. |
tab-size | number | 2 | Visual width of a tab. |
readonly | boolean | false | Make the code read-only. |
label | string | "" | Visible label; also the accessible name. |
confirm | boolean | false | Server-confirmed value: :state(pending) while the local value differs from the server's value attribute (see revert()). |
name | string | "" | Name reported in sb-change (e.g. the field of a command). |
Events
| Name | Description |
|---|---|
input | On every edit (native, re-targeted to the host). |
change | When the field loses focus after edits. |
sb-change | Same moment. detail: { name, value }: ready for a command. |
sb-run | Ctrl/Cmd+Enter. detail: { value }. |