Skip to content
omaris

Inputs

Inline Edit

Click the text, it becomes a field. Nothing on the page moves.

import { InlineEdit } from 'omaris'
Learn

Examples

Basic

Click the title. The line under it does not move: the reading view, the input and an invisible copy of the text all share one grid cell.

This line stays exactly where it is.

<script lang="ts">	import { InlineEdit } from 'omaris';​	let title = $state('Untitled board');</script>​<div class="w-96 max-w-full rounded-shape-md border border-border p-4">	<InlineEdit bind:value={title} label="Board name" class="text-xl font-semibold" />	<p class="pt-2 text-sm text-muted-foreground">This line stays exactly where it is.</p></div>

Multiline

A textarea that grows by the line. Enter is a newline here, ⌘/Ctrl+Enter saves, Escape cancels.

<script lang="ts">	import { InlineEdit } from 'omaris';​	let note = $state('Two lines of notes,\nand the box grows with them.');	let empty = $state('');</script>​<div class="flex w-96 max-w-full flex-col gap-4">	<InlineEdit bind:value={note} multiline block label="Notes" />	<InlineEdit bind:value={empty} placeholder="Add a description" block label="Description" /></div>

Async

A save that can fail. The value is only written once the promise settles, so a rejection leaves the field open on what was typed — with the reason floating under it, where it cannot push the page around.

<script lang="ts">	import { InlineEdit } from 'omaris';​	let name = $state('Anything but "no"');​	async function save(next: string) {		await new Promise((resolve) => setTimeout(resolve, 800));		if (next.toLowerCase() === 'no') throw new Error('That name is taken');	}</script>​<div class="w-80 max-w-full">	<InlineEdit		bind:value={name}		label="Name"		onsave={save}		validate={(next) => (next.length < 2 ? 'At least two characters' : null)}	/></div>

Overridden

A right-aligned figure in a table row: monospace, tabular, no pencil.

Revenue
Cost
<script lang="ts">	import { InlineEdit } from 'omaris';​	let revenue = $state('12 500');	let cost = $state('4 200');</script>​<div class="w-64 max-w-full overflow-hidden rounded-shape-md border border-border">	{#each [{ label: 'Revenue', get: () => revenue, set: (v: string) => void (revenue = v) }, { label: 'Cost', get: () => cost, set: (v: string) => void (cost = v) }] as row (row.label)}		<div class="flex items-center justify-between border-b border-border px-3 py-2 last:border-b-0">			<span class="text-sm text-muted-foreground">{row.label}</span>			<InlineEdit				value={row.get()}				onsave={row.set}				align="end"				size="sm"				pencil={false}				label={row.label}				class="font-mono tabular-nums"				classes={{ view: 'hover:bg-primary/10' }}			/>		</div>	{/each}</div>

When to use it

Use it for

  • Renaming the thing on screen: a board, a document, a column, a file. Opening a dialog to change six characters is too much.
  • A field in a detail panel or card that is read far more often than it is changed: a title, an owner, a note, a due-date label.
  • A cell in a light table of editable values. Use align="end" and pencil={false}.
  • A description. multiline grows the box by the line as it is typed.

Not for

  • A form → Text Field and a submit button. Click-to-edit fields hide that there is anything to fill in.
  • Something required before anything else works. An empty inline edit reads as "nothing here", not "required".
  • A value with a picker behind it, like a date, a currency or a country → Picker, Select or Price Input in a small form.
  • Text longer than a couple of lines → Textarea or Rich Text Editor.

Do

  • Give it a label. It is the accessible name in both states, read as "Board name: Untitled board", and there is no visible label to fall back on.
  • Return a promise from onsave. The field shows a spinner, writes value once it resolves, and stays open with the message if it rejects.
  • Use validate for the rules. The message floats under the box and never pushes the page around.
  • Set placeholder to what should be there: "Add a description". An empty box does not look editable. Turn actions on for touch, where there is no obvious way to leave the field; leave them off on desktop.
  • Put the type on the component (class="text-xl font-semibold"), not on the parts. Everything inside inherits it, so both states stay the same size.

Don't

  • Swap a <span> for an <input> yourself. The border, padding and line height all change, and the rows below jump twice per edit.
  • Put one inside a row that also handles clicks. The edit and the row's action fight over the same press.
  • Save on every keystroke. onsave fires once, on Enter or on leaving.
  • Use trigger="manual" without giving people something to press.

Quick reference

size
  • sm
  • md (default)
  • lg
align
  • start (default)
  • center
  • end

API

InlineEdit

Click the text, it becomes a field. Nothing on the page moves.

That last part is the whole component. The usual way to build this — swap a <span> for an <input> — changes the box's height, its padding and its font all at once, so the line jumps, the row below it jumps, and on a card the whole layout reflows twice per edit. Here the reading view, the input and an invisible copy of the text share one grid cell: the copy sizes the cell, the other two stretch to fill it, and all three carry identical type and padding. The box is the same size before, during and after — and it grows with what is typed, in one place, as you type.

import { InlineEdit } from 'omaris'
<InlineEdit bind:value={name} onsave={rename} /><InlineEdit bind:value={note} multiline variant="body-sm" />

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
The stack. Its size comes from the invisible copy inside it.
sizer
The invisible copy of the text. Nothing else sizes this component.
view
What you see when it is not being edited.
control
The control. Same font, same padding, same border box as the view.
pencil
The pencil. Sits in space that is reserved whether it shows or not.
placeholder
The text when there is none — "Add a description".
error
The error line. Floats under the box, so it never pushes anything.
actions
The confirm/cancel pair, when actions is on. Also floating.
action
One of those buttons.
busy
The spinner shown while an async onsave is in flight.

Props

value bindable

Defaults to ''

string

The text. Bindable, and only written once a save succeeds.

editing bindable

Defaults to false

boolean

Whether the field is open. Bindable — the way to open it from a button.

size

Defaults to 'md'

InlineEditSize
sm
md
lg
align

Defaults to 'start'

InlineEditAlign
start
center
end
multiline

Defaults to false

boolean

A textarea that grows by the line instead of an input.

block

Defaults to false

boolean

Fill the container rather than hugging the text.

trigger

Defaults to 'click'

InlineEditTrigger

What opens the field. manual leaves it to bind:editing.

submitOn

Defaults to 'both'

InlineEditSubmit

When the change is kept. Escape always cancels.

pencil

Defaults to true

boolean

Show the pencil on hover. Its space is reserved either way.

actions

Defaults to false

boolean

Float a confirm/cancel pair under the field — worth it on touch.

selectOnEdit

Defaults to true

boolean

Select the whole value when the field opens.

placeholder

Defaults to 'Empty'

string

Shown, greyed, when the value is empty.

label
string

Accessible name — what this text is. Required in practice.

disabled

Defaults to false

boolean
readonly

Defaults to false

boolean

Reads, never edits. The hover hint and the pencil go away.

maxlength
number
validate
(value: string) => string | null

Checked before saving. Return a message to refuse the change and keep the field open; return null to let it through.

onsave
(value: string) => void | Promise<unknown>

Called with the new text. Return a promise and the field waits on it, showing a spinner — a rejection keeps the field open with the message.

oncancel
() => void

Called when the edit is abandoned.

view
Snippet<[string]>

Replaces the reading view — a badge, an avatar beside the name.

class
string
classes
InlineEditClasses

Per-part Tailwind overrides. class still covers the root.

ref bindable

Defaults to null

HTMLInputElement | HTMLTextAreaElement | null