Code Editor

A light code field with pixel-theme highlighting, smart tabs and line numbers.

zweiundeins <sb-code-editor> code editor textarea syntax highlight Since 2026-09-210Open in playground Edit on GitHub

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

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://rocket.libretto.ch/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>

<!-- Or skip the autoloader and load just this component: -->
<!-- <script type="module" src="https://rocket.libretto.ch/c/code-editor/code-editor.js"></script> -->

API reference

Props

AttributeTypeDefaultDescription
language"js" | "html" | "css""js"Syntax to highlight.
valuestring""Initial code (or a child <script type="text/plain">). Read the live code from the value property.
line-numbersbooleantrueShow a line-number gutter.
tab-sizenumber2Visual width of a tab.
readonlybooleanfalseMake the code read-only.
labelstring""Visible label; also the accessible name.

Events

NameDescription
inputOn every edit (native, re-targeted to the host).
changeWhen the field loses focus after edits.
sb-runCtrl/Cmd+Enter. detail: { value }.