Skip to content
omaris

Inputs

Code Editor

A code editor with no editor in it.

import { CodeEditor } from 'omaris'
Learn

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.

utils.ts TypeScript
<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.

slugify.ts TypeScript
<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.

total.ts TypeScript
NORMAL Ln 1, Col 1 8 lines 99 chars
mode normal · :w written 0 times
<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.

package.json JSON
Ln 1, Col 1 5 lines 85 chars
read-only, numbered from 118
<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.

totals.ts TypeScript
Ln 1, Col 1 4 lines 80 chars
<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. lang takes 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. markers draw a rule under the span and put the message in the status bar; completions feed ⌃Space.
  • A tool for people who live in a terminal. vim is one prop.

Not for

  • Code that is shown, not edited → Code Block. readOnly here 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. plain has no colour, no bracket pairs and no comment token for ⌘/.
  • Size it with height, not a class. height="auto" plus a min-h-* on classes.frame for content-sized; a fixed number inside a dialog.
  • Wire onsave. ⌘S and vim's :w are caught either way and do nothing without it.
  • Turn copy on 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 wrap for markdown, SQL and anything prose-shaped. Leave it off for code, where a long line is a signal.

Don't

  • Switch vim on 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 readOnly to display a short snippet → Code Block.
  • Leave title off 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. copy already handles the selection, the flash and the insecure-origin fallback. actions is for the controls beside it: a language switch, a format button, a reset.

Quick reference

size
  • sm
  • md (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 Enter would 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.
mode
NORMAL, INSERT, …
command
The : line, and anything vim has to say.
pending
The keys of a half-typed vim command.

Props

value bindable

Defaults to ''

string

The source. Bindable.

lang

Defaults to 'plain'

string

ts, svelte, css, json, bash, … — the names CodeBlock takes.

size

Defaults to 'md'

CodeEditorSize
sm
md
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 bindable

Defaults 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 bindable

Defaults to null

HTMLTextAreaElement | null