Inputs
Code Editor
A code editor with no editor in it.
import { CodeEditor } from 'omaris' Examples
Basic
It is a real <textarea> with a highlighted copy painted behind it, so Tab indents, Enter keeps your indentation, brackets close themselves and ⌘Z still walks back one edit at a time. Type in it.
<script lang="ts"> import { CodeEditor } from 'omaris'; let value = $state(`export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs));}`);</script><CodeEditor bind:value lang="ts" title="utils.ts" height={220} class="w-full" /> Copy
copy puts a copy button in the header, or floats one when there is no header.
<script lang="ts"> import { CodeEditor } from 'omaris'; let withHeader = $state(`export function slugify(text) { return text .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '');}`); let bare = $state(`const total = items.reduce((sum, item) => sum + item.price, 0);`);</script><div class="flex flex-col gap-5"> <CodeEditor bind:value={withHeader} lang="ts" title="slugify.ts" copy height={160} /> <!-- No title, so the button floats over the corner: quiet until the pointer arrives, and always there on a touch screen. Select a few lines first and it copies the selection instead of the file. --> <CodeEditor bind:value={bare} lang="ts" copy height={96} lineNumbers={false} /></div> Vim
vim is the whole integration. Click in and you are in normal mode — the status bar says which one you are in and echoes the half-typed command. Counts, operators and motions (d2w, ci", >ip, gUiw, gcc), registers, marks, / search, ., <C-a>, and an ex line with :w, :%s/a/b/g and :set rnu. u undoes a whole command, not a keystroke.
<script lang="ts"> import { CodeEditor, Text, type VimMode } from 'omaris'; let mode = $state<VimMode>('normal'); let writes = $state(0); let value = $state(`export function total(list) { let sum = 0; for (const n of list) { sum += n; } return sum;}`);</script><div class="flex w-full flex-col gap-2"> <CodeEditor bind:value bind:vimMode={mode} lang="ts" vim title="total.ts" insertSpaces={false} height={240} onsave={() => writes++} /> <Text variant="label-sm" tone="muted"> mode {mode} · :w written {writes} {writes === 1 ? 'time' : 'times'} </Text></div> Status and read only
status puts line, column and length along the bottom. readOnly keeps the colouring and the gutter but refuses edits, and startLine numbers a snippet from wherever it really begins.
<script lang="ts"> import { CodeEditor, Text } from 'omaris'; let json = $state(`{ "name": "omaris", "type": "module", "peerDependencies": { "svelte": "^5.0.0" }}`); const EXCERPT = `.dark { --surface: oklch(0.21 0.01 260); --surface-container: oklch(0.26 0.012 260);}`;</script><div class="flex w-full flex-col gap-4"> <CodeEditor bind:value={json} lang="json" title="package.json" status height="9rem" /> <div class="flex flex-col gap-1"> <Text variant="label-sm" tone="muted">read-only, numbered from 118</Text> <CodeEditor value={EXCERPT} lang="css" readOnly size="sm" startLine={118} highlightActiveLine={false} height="7rem" /> </div></div> Find and diagnostics
⌘F finds and ⌘H replaces — every hit is drawn behind the text, so the count in the panel and the marks in the file agree. markers puts a rule under a span, colours its gutter number, and shows the message in the status bar while the caret is on that line. ⌃Space completes.
<script lang="ts"> import { CodeEditor } from 'omaris'; let value = $state(`const total = items.reduce((a, b) => a + b);let unused = 2;console.log(totl);`);</script><CodeEditor bind:value lang="ts" title="totals.ts" height={190} status completions={['items', 'total', 'reduce', 'console']} markers={[ { line: 2, column: 5, endColumn: 11, severity: 'warning', message: "'unused' is declared but never read" }, { line: 3, column: 13, endColumn: 17, severity: 'error', message: "Cannot find name 'totl'. Did you mean 'total'?" } ]} class="w-full"/> Overridden
height lives in a custom property rather than a class, so height="auto" plus a min-h-* on classes.frame lets the editor grow with its content instead of scrolling. wrap stops the sideways scroll, and the gutter can go.
<script lang="ts"> import { CodeEditor } from 'omaris'; let value = $state( `# every line here is long enough to prove that wrapping is on rather than a horizontal scrollbar\nbun run check && bun run lint && bun run reference` );</script><CodeEditor bind:value lang="bash" wrap lineNumbers={false} tabSize={4} autoClose={false} height="auto" classes={{ root: 'rounded-shape-lg border-primary/40', frame: 'min-h-24', input: 'caret-primary' }} class="w-full"/> When to use it
Use it for
- Source someone edits inside an app: a webhook payload, a SQL query, a JSON config, a template.
langtakes the same names as Code Block. - An editor that ships small. It is a
<textarea>with a highlighted copy behind it, so IME, autocorrect, undo and screen readers are the browser's. - Feedback from your own validator.
markersdraw a rule under the span and put the message in the status bar;completionsfeed⌃Space. - A tool for people who live in a terminal.
vimis one prop.
Not for
- Code that is shown, not edited → Code Block.
readOnlyhere still costs a gutter and a textarea. - Two versions of a file → Diff Viewer.
- Prose with bold and a list → Rich Text Editor.
- A log that keeps growing → Log Viewer. A JSON document to browse and fold → JSON Viewer.
- A multi-file IDE with a language server → Monaco or CodeMirror.
Do
- Always set
lang.plainhas no colour, no bracket pairs and no comment token for⌘/. - Size it with
height, not a class.height="auto"plus amin-h-*onclasses.framefor content-sized; a fixed number inside a dialog. - Wire
onsave.⌘Sand vim's:ware caught either way and do nothing without it. - Turn
copyon for anything someone is more likely to take away than edit: a generated snippet, a payload, a token. It copies the selection or the whole file, keeps the caret, and sits in the header when there is one. - Turn on
wrapfor markdown, SQL and anything prose-shaped. Leave it off for code, where a long line is a signal.
Don't
- Switch
vimon by default in a form. It is a preference someone opts into; normal mode swallows the first keystrokes of anyone who did not. - Hand it a file of tens of thousands of lines. The painted copy is the whole text; a log belongs in Log Viewer.
- Use
readOnlyto display a short snippet → Code Block. - Leave
titleoff when the person has more than one file open in their head. The header bar with a filename tells them which this is. - Build your own copy button out of
actions.copyalready handles the selection, the flash and the insecure-origin fallback.actionsis for the controls beside it: a language switch, a format button, a reset.
Quick reference
size smmd(default)
API
CodeEditor
A code editor with no editor in it.
It is a real <textarea> with a highlighted copy of the same text painted exactly behind it — which sounds like a trick and is actually the reason to do it. Native text editing is hard: IME composition, mobile autocorrect, spellcheck, undo that survives a page's own edits, screen readers, drag-and-drop of selected text, the Home key doing what your OS says it does. A contenteditable rewrites all of it, badly. A textarea gets all of it for free, and the only thing it lacks is colour — so colour is the only thing added.
On top of that sit the things a plain textarea genuinely lacks: Tab indents a selection, Enter keeps your indentation, brackets close themselves, ⌘/ comments, ⌥↑/⌥↓ move a line and ⌥⇧↑/⌥⇧↓ duplicate it, ⌘F finds and replaces, ⌘D walks the occurrences of a word, ⌃Space completes it, and every edit goes through the browser's own insert command so ⌘Z still walks back one change at a time.
And one prop turns it into vim — modes, operators, text objects, registers, marks, / search, ., and an ex line with :s/// and :set:
import { CodeEditor } from 'omaris' <CodeEditor bind:value lang="ts" lineNumbers height={320} /> <CodeEditor bind:value lang="ts" vim /> Parts
Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.
root- No description in the source yet.
header- Filename bar.
title- No description in the source yet.
badge- No description in the source yet.
tools- The header's trailing group — your
actions, then the copy button. float- The copy button when there is no header to hold it. Quiet until the pointer is over the editor, and always there on a touch screen, where there is no hover to reveal it with.
frame- Gutter and text, side by side.
gutter- The line-number column. Scrolls with the text, never sideways.
gutterInner- No description in the source yet.
gutterLine- One number. Its height must match a text line exactly.
gutterMarked- A gutter number on a line that has a diagnostic.
editor- Holds the painted copy and the real textarea, exactly on top of each other.
paint- The highlighted copy. Never interactive — it is a picture of the text. In flow, so
height="auto"has a content height to grow to. code- No description in the source yet.
line- One painted line.
active- The line the caret is on.
overlay- Search hits, the bracket pair, diagnostics, the vim caret.
overlayInner- No description in the source yet.
match- One search hit.
matchCurrent- The hit
Enterwould jump to. bracket- The bracket under the caret and its partner.
caret- Vim's block caret.
marker- The rule under a diagnostic.
guide- One indent-guide rule.
input- The textarea. Transparent text, visible caret — the paint shows through.
placeholder- Shown over an empty editor.
find- Find and replace, floating over the top-right of the text.
findRow- No description in the source yet.
findField- No description in the source yet.
findButton- No description in the source yet.
findCount- No description in the source yet.
menu- The completion list.
option- No description in the source yet.
optionActive- No description in the source yet.
status- Line, column and length.
modeNORMAL,INSERT, …command- The
:line, and anything vim has to say. pending- The keys of a half-typed vim command.
Props
value bindableDefaults to ''
string The source. Bindable.
lang Defaults to 'plain'
string ts, svelte, css, json, bash, … — the names CodeBlock takes.
size Defaults to 'md'
CodeEditorSize smmd
title string Filename, shown in the header bar.
badge boolean Show the language chip. Defaults to on whenever there is a header.
copy Defaults to false
boolean Offer a copy button. It goes in the header when there is one, and floats over the top corner when there is not — where it fades in on hover, and stays put on a touch screen.
It copies the selection when there is one and the whole file when there is not, and it never takes the caret out of the editor.
actions Snippet Extra header controls, before the copy button.
oncopy (text: string) => void Fires after the source lands on the clipboard, with what was copied.
lineNumbers Defaults to true
boolean relativeLineNumbers Defaults to false
boolean Number every line by its distance from the caret, as :set rnu does.
startLine Defaults to 1
number Number the first line is given.
wrap Defaults to false
boolean Wrap long lines instead of scrolling sideways.
highlightActiveLine Defaults to true
boolean Tint the line the caret is on.
readOnly Defaults to false
boolean tabSize Defaults to 2
number Indent width, in spaces.
insertSpaces Defaults to true
boolean Indent with spaces rather than a tab character.
autoClose Defaults to true
boolean Close brackets and quotes as you type, and wrap a selection in them.
status Defaults to false
boolean Line, column and character count along the bottom. On by default with vim.
vim Defaults to false
boolean Modal editing: normal, insert, visual and visual-line, operators and motions (d2w, ci", >ip, gUiw), registers, marks, / search, ., <C-a>, and :w :q :s/// :set. u and <C-r> undo a whole command — ciwfoo<Esc> included — and still leave ⌘Z working.
vimMode bindableDefaults to 'normal'
VimMode The mode vim is in. Bindable, for a status bar of your own.
find Defaults to true
boolean ⌘F / ⌘H.
bracketMatch Defaults to true
boolean Ring the bracket under the caret and its partner.
indentGuides Defaults to false
boolean A rule down each level of indentation.
markers Defaults to []
CodeEditorMarker[] Errors and warnings to draw under the text.
completions string[] | ((context: CodeEditorCompletionContext) => string[]) What ⌃Space offers. A list, or a function called with the word being typed. Given a list, the menu also opens as you type. Left out, ⌃Space still completes from the words already in the document.
comment string | [string, string] What ⌘/ and vim's gc comment with. A line token like '#', or an open/close pair. Defaults to the one lang uses.
onsave (value: string) => void ⌘S, and vim's :w.
onquit () => void Vim's :q. Nothing happens without it.
onchange (value: string) => void height Defaults to '18rem'
number | string Height of the editing area. A number is px, a string any CSS length. It lives in a property rather than a class, so a height in classes.frame genuinely replaces it — height="auto" with a min-h-* class lets it grow with the content instead.
class string classes CodeEditorClasses ref bindableDefaults to null
HTMLTextAreaElement | null