jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

registry:ui · Data Entry

textarea — the multiline text shell

NativeHTML textarea with the jixoai text shell; rows pass-through (default 4), resize: vertical. Same label/error contract as input.

install — textarea

$npx jixoai-ui add textarea

https://ui.jixoai.com/r/textarea.json

resize: vertical toolbar + status slot rows count readout label[for] + aria wiring attribute-transparent

overview

Overview

The platform gives the multiline field; the family gives the text-shell law, six slot seams, a count readout with announcement discipline, and the label/error contract — all measured.

The native element carries the semantics: a real <textarea> — multiline text, platform forms, FormData and form.reset untouched. The family adds the text-shell law (the same one as input: a 1px var(--border) frame over var(--background), radius 0, a hover lift, and the site's inset 1px focus-visible outline on the ring token), locks resize to the vertical axis (never horizontal — it would break column rhythm), and wires the field contract: label[for] with an auto id, an error string that becomes aria-invalid + aria-describedby + a dashed shell + the "! message" line, and a count readout. rows defaults to 4; everything else — placeholder, maxlength, disabled, name, required, spellcheck, wrap — flows through restProps onto the element.

The shell is a slot system. Inner block rows land inside the frame behind 1px hairlines (a toolbar above, a status row below); outer rows replace or extend the field's surroundings; and count appends an N / maxLength readout with announcement discipline: code-point counting (a surrogate pair counts ONE — measured 🫠🫠 = 2), aria-live off everywhere except the last 10% of the cap, where it flips to polite — near the limit the count is worth announcing, elsewhere it never chatters per keystroke. value is bindable: bound ⇒ controlled; absent ⇒ purely uncontrolled (Svelte skips undefined writes, so FormData and form.reset keep native behavior — measured). A caller-supplied oninput still fires after the family syncs.

The eight axes: density is no-opinion and REACTIVE — the family owns no density paint, and the ambient scope channels move the field (measured 10px/60px at 2xs vs 15px/90px at lg); size and color are CONSUMED by the §1 collision rule (the native element never receives them — measured); theme is a declarative stamp whose story has three measured strata (page bridge → canvas light-island → component stamp, closest scope wins); shape/radius/elevation/motion supply unread (zero carrier readers, grep receipts). Kinship: input (the single-line twin — same shell law), combobox and select (the other field contracts).

textarea

the multiline half of the shell family — label + count rows, locked vertical resize, and the same label/error contract as input; value is $bindable.

21
textarea-usage.svelte svelte
<Textarea
  label="notes"
  rows={4}
  bind:value
  count
/>

W3C foundation

What the platform gives

The native textarea keeps platform semantics — multiline text, FormData, form.reset, the attribute set (placeholder, maxlength, spellcheck, wrap…). The shell only paints: same 1px border, radius 0, hover lift, and inset focus outline as the input shell — only taller.

usage
<Textarea label="notes" rows={5} maxlength={280} />

<!-- rows pass through (default 4); resize locks to the vertical axis;
     every other attribute rides through restProps -->
<Textarea label="bio" rows={3} placeholder="multiline, maxlength rides through…" maxlength={280} spellcheck={false} wrap="hard" />

types

Textarea variants

The plain shell, the toolbar + count posture, the error state, and the disabled field.

textarea · variants

0 / 280

bio is required

textarea-types-demo.svelte svelte
<script lang="ts">
  import Textarea from '@ui/textarea.svelte';
</script>

<div class="grid gap-4 sm:grid-cols-2">
  <div class="border border-border p-4"><Textarea label="plain" name="types-plain" rows={3} placeholder="multiline…" /></div>
  <div class="border border-border p-4"><Textarea label="count" name="types-count" rows={3} maxlength={280} count placeholder="N / maxLength readout…" /></div>
  <div class="border border-border p-4"><Textarea label="error" name="types-error" rows={2} error="bio is required"></Textarea></div>
  <div class="border border-border p-4"><Textarea label="disabled" name="types-disabled" rows={2} placeholder="not allowed" disabled /></div>
</div>

textarea

Slot system — toolbar + status rows

textarea adds inner-block-start / inner-block-end behind 1px hairlines for toolbar and status rows, plus count for an N / maxLength readout. The shell owns border, background, hover, and the inset focus outline, so slot content never repaints the box law — and value is $bindable: a bound field turns controlled, an unbound one stays purely uncontrolled (FormData and form.reset untouched).

bold italic code link
draft — autosaves on blur 21 / 280

Slot content lands muted at 0.75rem behind the hairline rows; the wrapper is scoped, so override it with an important utility (text-foreground!) or an inline style. The count readout lives in the status row — N / maxLength, counted in code points off the mirrored value, never a second source of truth.

slots
<!-- textarea · 6 slots — inner rows sit behind 1px hairlines -->
<Textarea label="notes" rows={6} maxlength={280} count bind:value={text}>
  {#snippet innerBlockStart()}<span>bold</span><span>italic</span>{/snippet}
  {#snippet innerBlockEnd()}<span>draft</span>{/snippet}
</Textarea>

law

label + error wiring

The error prop is pure semantics: it sets aria-invalid='true', wires aria-describedby to the “! message” line, and dashes the shell border — the border signal is monochrome on purpose (shape, not hue), while the message line itself rides var(--destructive).

bio is required

error
<Textarea label="bio" error="bio is required" rows={2}></Textarea>

<!-- error wiring, the family law:
  · aria-invalid="true" on the control
  · aria-describedby → the "! message" line (var(--destructive) ink)
  · border-style: dashed on the shell — the BORDER signal is
    monochrome on purpose (shape, not hue) -->

usage

Usage

rows and every other native attribute pass through; the component adds the label/error contract, the hairline slot rows, and the count readout.

Textarea usage
<Textarea label="notes" rows={5} maxlength={280} />

<!-- rows pass through (default 4); resize locks to the vertical axis;
     every other attribute rides through restProps -->
<Textarea label="bio" rows={3} placeholder="multiline, maxlength rides through…" maxlength={280} spellcheck={false} wrap="hard" />

theming

Density and tokens

The shell, lane padding, and label/error rhythm are pure density-scope tokens — the field is density-REACTIVE (measured 10px voice / 60px height at the 2xs stamp vs 15px / 90px at lg (rows-4 projections: height = rows × voice × 1.5 — the served rows-3 sample reads 45/68, the same arithmetic)) — and resize the scope and the whole field stack follows.

2xs
xs
sm
default
lg
TokenDefaultSource
--jx-hit28 / 32 / 40 / 48pxdensity
--jx-text11 / 12 / 13 / 15pxdensity
--jx-line16 / 18 / 20 / 24pxdensity
--jx-inset8 / 8 / 12 / 16pxdensity
--jx-stack4 / 4 / 8 / 8pxdensity

api

API

The declared surface is 20 named props (label, error, count, rows, id, value, chrome, class + the four slot snippets + the eight axes) — and the component is ATTRIBUTE-TRANSPARENT, not rest-less: the native textarea attribute set rides the rest spread onto the real element, forwarded BY NAME where the family gives it a job (maxlength drives the count readout; rows defaults to 4; oninput fires after the family syncs) and verbatim everywhere else (placeholder, disabled, name, required, spellcheck, wrap, minlength, readonly…). Two withholdings: the color ATTRIBUTE is Omitted (the color AXIS is not), and a caller data-density is captured by the axis stamp.

Properties

PropertyTypeDefaultDescription
label string—Field label rendered as label[for] above the control; skipped when outerBlockStart takes the slot over.
error string—Error text: sets aria-invalid="true", wires aria-describedby to the "! message" line, dashes the shell.
count booleanfalseAppends an "N / maxLength" readout to the inner-block-end row (code-point counting; aria-live flips to polite at 90% of the cap).
rows number4Native rows attribute, passed through verbatim.
id string$props.id()Wired into label[for] and the error aria-describedby; auto-generated when omitted.
value bindstring | number—Bindable; bound ⇒ controlled, absent ⇒ purely uncontrolled (FormData and form.reset keep native behavior).
chrome 'frame' | …ambient ?? 'frame'The control-chrome lane; the ambient value comes from the ControlChrome context.
innerBlockStart Snippet—Inside the shell, above the textarea (toolbar row, behind a hairline).
innerBlockEnd Snippet—Inside the shell, below the textarea (status row).
outerBlockStart Snippet—Outside the shell, above — replaces the label row when given.
outerBlockEnd Snippet—Outside the shell, below — renders below the error line.

Universal props

PropertyTypeDefaultDescription
size'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopethe base scale — root font-size; parts size in em
shape'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto' · ambient scopecorner geometry (CSS corner-shape; §14 degrade table)
radius'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopecorner size; auto = the concentric broadcast (§3)
density'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopespacing/leading scale over the kernel channels (§4)
color'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto' · ambient scopethe hue axis of the fixed oklch primary system (§5)
theme'light' | 'dark' | 'system' | 'auto''auto' · ambient scopelight/dark profile; system = the JS-mutable global (§6)
elevation'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto' · ambient scopeofficial M3 levels over the surface ladder (§7)
motion'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto' · ambient scopeintensity across the motion kernels (§8)

axes

The eight axes on textarea

Density is NO-OPINION AND REACTIVE — the family owns no density paint and the ambient scope channels move the field (measured 10px/60px at 2xs vs 15px/90px at lg); size and color are CONSUMED by the §1 collision rule (the native element never receives them); theme is a declarative stamp with three measured strata (page bridge → canvas light-island → component stamp, closest scope wins); shape/radius/elevation/motion supply unread (zero carrier readers).

PropertyTypeDefaultDescription
density '2xs' | 'xs' | 'sm' | 'default' | 'lg' | 'auto' | number (+ the five legacy spellings)'auto'NO-OPINION AND REACTIVE — the family owns no density paint (every Defaults slot is the no-opinion axis slot), so the AMBIENT scope channels flow straight through the field: --jx-text/--jx-line/--jx-inset/--jx-hit move the voice and the height (measured 10px voice / 60px height at the 2xs stamp vs 15px / 90px at lg (rows-4 projections: height = rows × voice × 1.5 — the served rows-3 sample reads 45/68, the same arithmetic)). The rung attr stamps the field root (data-density). Zero --jx-density-effective readers (grep receipt) — the channels are plain density-scope custom properties, not axis carriers. Number unit: coefficient.
size 'small' | 'medium' | 'large' | 'auto' | number'auto'CONSUMED (§1) — the native element NEVER receives a size attribute from this axis (the §1 native collision rule; measured: size-stamped fields carry no size attribute); the §1 stamp scales the field root and the field voice follows (measured echo). Zero --jx-size-effective readers (grep receipt). Number unit: px.
shape 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto'SUPPLY-ONLY — zero shape-channel readers (grep receipt: no corner-shape or factor consumer in ui/textarea/); the corner is the consumed jx-pure box law (measured 0px — square chrome). Number unit: none.
radius 'small' | 'medium' | 'large' | 'auto' | number'auto'SUPPLY-ONLY — zero radius-effective readers (grep receipt); the shell radius is the consumed box law (measured 0px), and a stamped carrier moved nothing (measured). Number unit: px.
color 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto'CONSUMED (§1) AND SUPPLY-ONLY — the hue axis never reaches the native element (the collision rule, measured); the shell paints var(--border)/var(--background) from the theme profile and the family’s one semantic color is the error line’s var(--destructive). Zero --jx-color-effective readers (grep receipt). Number unit: hue degrees.
theme 'light' | 'dark' | 'system' | 'auto''auto'DECLARATIVE STAMP, AND THE HOST DECIDES — three strata, measured. L1 page bridge: under prefers-dark the SITE'S THEME SYNC adds html.dark (the mechanism — not the media query alone) and --border flips oklch(0 0 0) → oklch(1 0 0) page-wide and a body-level field re-derives. L2 host island: inside a component-canvas stage a data-theme="light" island re-pins the light profile — the served demo does NOT re-derive (island beats page). L3 component stamp: theme="dark" stamps the .dark class on the field root — the closest scope wins over the island. The ink has two halves: the shell face reads base tokens and flips at each stratum, but the typed text rides the ROOT-PINNED ALIAS chain and holds the root resolution — at the settled L3 stamp that is black text on the flipped shell (latent — no served L3 demo; the family ink-pair repair is queued, W-next #7). The invalid signal stays monochrome at every stratum (a dashed border is shape, not hue). system/auto = tree inheritance. No number lane.
elevation 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto'SUPPLY-ONLY — zero elevation-carrier readers (grep receipt); the only shadow is the consumed box law’s hover lift (measured a 1px inset shadow — not a shadow tier). Number unit: dp.
motion 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto'SUPPLY-ONLY — zero --jx-motion-effective readers (grep receipt); the shell’s color/outline transitions ride the consumed jx-pure law, and the count readout’s announcement discipline (aria-live off → polite at 90% of the cap) is a semantic channel, not a motion one. Number unit: coefficient.

Receipts: the shell law (resize locked vertical; radius 0; hover lift measured as a 1px inset shadow; focus-visible the inset 1px ring outline at offset −1px; rows 4 default; label[for] auto-wired) and the error contract (aria-invalid="true", aria-describedby resolving to the "! message" line, dashed shell) were measured on this page's served DOM (probe, task 68); the count readout (N / maxLength; code-point counting measured 🫠🫠 = 2; aria-live off → polite at 90% of the cap and back) and the native forwarding (maxlength, spellcheck, wrap, name read back off the element) the same way; the theme strata (body-level border oklch(0 0 0) → oklch(1 0 0) under page-dark, the canvas data-theme="light" island pinning the served demo, the component .dark stamp winning closest) were measured across two loads; the unread rows carry grep receipts over ui/textarea/. The query() seat below rides the md viewport key (48rem) on the size lane.

one real query() case
<script lang="ts">
  import Textarea from '@ui/textarea.svelte';
  import { query } from '@lib/universal-props-query.svelte';
</script>

<!-- below 48rem the base (13px root) applies; at 48rem+ the md case (18px)
     wins — the field voice follows, the native element never sees a size
     attribute -->
<Textarea label="notes" size={query({ md: 18 }, 13)} rows={3} />

textarea · query()

Media keys are min-width: below 48rem the base (13px root) applies; at 48rem and wider the md case wins (18px) — the field voice follows while the native element never sees a size attribute. The number lane goes bare. Resize across 48rem.

textarea-query-demo.svelte svelte
<script lang="ts">
  import Textarea from '@ui/textarea.svelte';
  import { query } from '@lib/universal-props-query.svelte';
</script>

<!-- below 48rem the base (13px root) applies; at 48rem+ the md case (18px)
     wins — the field voice follows, the native element never sees a size
     attribute -->
<Textarea label="notes" size={query({ md: 18 }, 13)} rows={3} />

textarea · universal props

textarea-universal.svelte svelte
<Textarea label="px number" size={14} density="small" rows={2} />
<Textarea label="named steps" size="large" radius="medium" rows={2} />

a11y

Accessibility

The native textarea keeps platform semantics; the component wires the label and the validation message to the control, and the count readout announces only when it matters.

Keyboard

KeyAction
TabMoves focus to the textarea
any textTypes into the field
dragResize handle, locked to the vertical axis

ARIA

AttributeValueDescription
label[for]{id}The label prop names the control (auto id; skipped when outerBlockStart takes the slot over — author your own wiring then)
aria-invalid'true'Set on the native textarea when the error prop is provided (measured)
aria-describedby{id}-errorPoints at the "! message" validation line — a static read on focus, no live region chatter while typing
aria-liveoff → politeThe count readout stays silent until 90% of maxlength, then announces politely (aria-atomic); measured flipping both ways
aria-invalid paintdashed borderThe shell’s invalid signal is monochrome (border-style dashed) — the message line carries var(--destructive)
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h