registry:ui · Data Entry
input — every native type, one shell
NativeHTML input: every native type passes through. Text-like types get the jixoai shell (1px border, radius 0, hover lift, inset focus ring) with the Tier-1 lane (.jx-input-lane) inside; range is the Tier-1 pure-CSS slider (.jx-range); color is the Tier-1 color field (.jx-color-field wrapper, locked square swatch, pipette glyph); date/time/number lanes restyle the platform's picker indicator and spinners via the same sheet. checkbox/radio/toggle/file have their own components (the bare types still pass through). Optional label[for], error wiring (aria-invalid + aria-describedby, monochrome dashed ! message), clearable ×, four snippet slots. value is $bindable: bound = controlled, absent = purely uncontrolled (FormData/form.reset untouched).
input
The text-shell base of the NativeHTML family: every native type passes through untouched — the component owns only label/error wiring, four slot seams, and the bordered shell.
<Input
type="text"
label="endpoint"
placeholder="you@host.tld"
clearable
bind:value
/>input
All native types
One component, every type the platform ships. Text-like types take the bordered shell; range and color get full Tier-1 native repaints (the pure-CSS slider and the swatch-plus-pipette color field); the date/datetime-local/week/month/time lanes open embedded Popover-API panels behind the repainted ink indicator, and number swaps its platform spinner for the custom −/+ stepper pair (↑/↓ stepping stays). checkbox and radio split into their own pure-CSS components, and file picking and dates have their professional homes in file-input and date-picker — those native types still pass through here as the bare controls.
input · all native types
<script lang="ts">
import Input from '@ui/input.svelte';
import CardGrid from '$lib/ui/card-grid/card-grid.svelte';
</script>
<CardGrid min="230px">
<div class="demo-cell" data-no-subgrid>
<Input type="text" label="text" name="demo_text" placeholder="plain text" />
</div>
<div class="demo-cell" data-no-subgrid>
<Input type="password" label="password" name="demo_password" placeholder="••••••••" />
</div>
<div class="demo-cell" data-no-subgrid>
<Input type="email" label="email" name="demo_email" placeholder="you@host.tld" />
</div>
<div class="demo-cell" data-no-subgrid>
<Input type="number" label="number" name="demo_number" placeholder="42" min="0" />
</div>
<div class="demo-cell" data-no-subgrid>
<Input type="number" label="number (native-controls)" name="demo_number_native" placeholder="42" min="0" nativeControls />
</div>
<div class="demo-cell" data-no-subgrid>
<Input type="search" label="search" name="demo_search" placeholder="grep…" />
</div>
<div class="demo-cell" data-no-subgrid>
<Input type="date" label="date" name="demo_date" />
</div>
<div class="demo-cell" data-no-subgrid>
<Input type="datetime-local" label="datetime-local" name="demo_datetime" />
</div>
<div class="demo-cell" data-no-subgrid>
<Input type="time" label="time" name="demo_time" />
</div>
<div class="demo-cell" data-no-subgrid>
<Input type="week" label="week" name="demo_week" />
</div>
<div class="demo-cell" data-no-subgrid>
<Input type="month" label="month" name="demo_month" />
</div>
<div class="demo-cell" data-no-subgrid>
<Input type="range" label="range" name="demo_range" min="0" max="100" />
</div>
<div class="demo-cell" data-no-subgrid>
<Input type="color" label="color" name="demo_color" value="#007924" />
</div>
<div class="demo-cell" data-no-subgrid>
<Input type="text" label="disabled" name="demo_disabled" placeholder="not allowed" disabled />
</div>
</CardGrid>Tab through the grid: every control is keyboard-reachable with its platform behavior — the color swatches, the range arrows, the date/time pickers (the calendar indicator is repainted through a mask, and clicking it opens the embedded panel — the custom controls are the default; pass native-controls for the platform ones). checkbox and radio live in their own pure-CSS components, file picking and dates have their professional controls; everything else is painted by the Tier-1 class vocabulary — range tracks and thumbs, color swatches, date/time indicators and the placeholder-vs-value distinction. The number lane splits by layer: bare Tier-1 markup keeps the platform spinner (engines reject custom paint on it, and it is the only zero-JS stepper there is — the D3 ruling), while the component fields above take over with the −/+ stepper pair (hold to accelerate; ↑/↓ stepping stays). week and month are platform popups only on Chromium — Firefox ships no control for them at all (the lanes degrade to plain text), so there the embedded panel is the only control on the engine.
Tier 1 — the pure-CSS native layer
Every native lane above is painted by ONE stylesheet — jx-pure.css (registry item jx-pure, imported once after the token sheet; the
class vocabulary is its Part A — native-form remains
a deprecated same-source alias) —
with a daisyui-style class vocabulary and zero JS. The same classes the components
consume style bare markup; type in the first field and watch the placeholder read
clearly lighter than a value:
<!-- import once, after the token sheet:
@import './lib/jixoai.css';
@import './lib/jx-pure.css'; -->
<label class="jx-label" for="deploy">deploy</label>
<input class="jx-control" id="deploy" type="datetime-local" />
<!-- range: pure-CSS slider — zero-JS fill to the thumb, thick strip + thin groove, ringed disc thumb -->
<input class="jx-slider" type="range" min="0" max="100" value="40" />
<!-- color: the wrapper label opens the picker from the pipette glyph;
compact 7:4 chip by default — add jx-color-expand for the row -->
<label class="jx-color-shell">
<input class="jx-color-swatch" type="color" value="#007924" />
</label>
<!-- companions? the shell wrapper owns the box, the input is chromeless -->
<label class="jx-control-shell jx-slotted">
<span>https://</span>
<input class="jx-control-lane" placeholder="api.jixoai.com" />
</label>input
Slot system — the InputGroup posture
The shell is a slot host: outer-block-start (takes the label row's place when given), inner-inline-start / inner-inline-end (inside the shell, muted by default, gap-2 seams), and outer-block-end (below the shell — the error line still renders above it). 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).
Type into the search field: the × only appears when there is something to clear, and
pressing it empties the DOM value, syncs the binding, and re-emits input plus a bubbling clear event — uncontrolled FormData flows and controlled
bindings both stay correct. Slot content lands muted at 0.75rem; the wrapper is scoped,
so override it with an important utility
(text-foreground!) or an inline style.
<!-- input · 4 slots — the shell owns the box law -->
<Input label="endpoint" placeholder="api.jixoai.com">
{#snippet innerInlineStart()}<span>https://</span>{/snippet}
{#snippet innerInlineEnd()}<span class="text-foreground!">/v1/spawn</span>{/snippet}
{#snippet outerBlockEnd()}<span>helper text below the shell</span>{/snippet}
</Input>
<!-- × clears the DOM value, syncs the binding, re-emits input + clear -->
<Input type="search" label="search" clearable bind:value={q} />
<!-- price: both inner seams at once, muted at 0.75rem -->
<Input label="price" placeholder="0.00">
{#snippet innerInlineStart()}<span>$</span>{/snippet}
{#snippet innerInlineEnd()}<span>per seat / mo</span>{/snippet}
</Input>input
count · reveal · floating label
Three family capabilities: a code-point character count with a near-limit live region, the password reveal eye (default on — but the value starts hidden), and the floating bracket label that rides the shell's top border like a fieldset legend.
0 / 20
off → polite from 90% of the cap0
email is required
The count readout lives in the hint lane below the shell and counts CODE POINTS —
surrogate pairs (emoji, ext-B CJK) are one character, never two UTF-16 units — while the
maxlength clamp itself stays the platform's. The reveal eye mounts by default on type="password" but starts HIDDEN (aria-pressed="false"):
pressing it flips only the input's type between
password/text, so autocomplete and password-manager behavior are untouched, and it takes
the outermost end-lane seat (snippet > clearable × > eye, each keeping the
--jx-hit edge-lane geometry). The floating label is the terminal translation: the label
NEVER enters the field — it rides the top border as a bracket, and its ink follows the
lane through pure CSS (empty muted, focused or filled foreground, error destructive).
<!-- count: the "n / max" code-point readout in the hint lane
(CJK and emoji count as one character); aria-live stays off until
the value crosses 90% of the maxlength cap, then reads polite -->
<Input label="bio" count maxlength={20} bind:value />
<!-- password reveal: ON by default, but the VALUE starts hidden — the
eye only flips the input's type between password/text (aria-pressed
mirrors the state; autocomplete/password managers untouched) -->
<Input type="password" label="api key" bind:value />
<!-- opt out with the bare false -->
<Input type="password" label="api key" reveal={false} />
<!-- end-lane order when the seam fills: snippet > clearable × > eye -->
<Input type="password" label="key" clearable>
{#snippet innerInlineEnd()}<span class="text-foreground!">owned</span>{/snippet}
</Input>
<!-- floating: the terminal BRACKET — the label rides the shell's top
border like a fieldset legend (no SaaS in-field morph); the ink
states are pure CSS (empty muted / focused+filled foreground /
error destructive) -->
<Input label="email" labelMode="floating" placeholder="you@host.tld" />
<Input label="email" labelMode="floating" error="email is required" />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 — a monochrome invalid signal, because the one-hue law has no error red.
email is required
a query is required
<Input
type="email"
label="email"
error="email is required"
/>
<!-- error wiring, the family law:
· aria-invalid="true" on the control
· aria-describedby → the "! message" line
· border-style: dashed on the shell (monochrome invalid signal —
the one-hue law has no error red) -->bridge
custom picker bridge
The native popups cannot be styled — so the swap is the default. date/datetime-local ride the date-picker Calendar (datetime-local adds a time-stepper row, and a day-pick keeps the panel open for the time adjustment), week rides a Calendar day-pick that commits the ISO week, month rides a year-nav + 12-month grid, time rides the custom HH:MM stepper (live commits), and color rides the color-picker Swatches — all through a Popover-API panel; the input stays a real input: native typing, parsing, ARIA and FormData untouched. The bare boolean attribute — native-controls, the disabled-attribute philosophy — opts back into the platform controls (number's platform spinner included), and a picker snippet overrides the default panel for anything else (its ctx carries value, commit and close).
committed: — · swatch: #7c7c7c · week: —
The week panel picks a DAY on the Calendar and commits the ISO week
(YYYY-Www, the picked week tinted); the month panel
navigates years and commits YYYY-MM from a 12-month
grid; the time panel is the custom HH:MM stepper, committing live as the arrows run —
the cells are slider-grade: the wheel over a group steps its number, press-drag moves
it vertically (up increases, the cells wear the ns-resize cursor), and an unset value
still shows the 00:00 digits.
The datetime-local panel is the Calendar plus a time-stepper row — a day-pick keeps
the panel open so the time can be adjusted before it closes, and the commit carries
both halves. The panels' vocabulary — month label, weekday heads, month cells —
renders through Intl.DateTimeFormat: the page's
own <html lang> is the default, or pass locale per field (the zh-CN sample above renders
2026年8月 / 周一…). On Firefox week and month have no platform control at all (the lanes
degrade to plain text), so there the embedded panel is the only control on the
engine.
<!-- the custom controls are the DEFAULT (number → the −/+ stepper,
date/datetime-local/week/month/time → embedded Popover-API panels,
color → Swatches) -->
<Input type="number" bind:value />
<Input type="date" bind:value onselect={(v) => …} />
<Input type="color" bind:value />
<!-- the bare attribute opts back into the platform controls
(the disabled-attribute philosophy: presence = true) -->
<Input type="date" native-controls />
<!-- override, not the only way: week already ships the embedded panel
(Calendar day-pick commits the ISO week) — a snippet REPLACES it -->
<Input type="week">
{#snippet picker(ctx)}
<MyWeekGrid value={ctx.value} onpick={ctx.commit} />
{/snippet}
</Input>types
Input variants
One component, four rendering lanes: the text-like shell, the clearable search field, the error state, and the disabled field.
input · variants
email is required
<script lang="ts">
import Input from '@ui/input.svelte';
</script>
<div class="grid gap-4 sm:grid-cols-2">
<div class="border border-border p-4"><Input type="text" label="text" name="types-text" placeholder="plain text" /></div>
<div class="border border-border p-4"><Input type="search" label="search" name="types-search" placeholder="grep…" clearable /></div>
<div class="border border-border p-4"><Input type="email" label="error" name="types-error" value="not-an-email" error="email is required" /></div>
<div class="border border-border p-4"><Input type="text" label="disabled" name="types-disabled" placeholder="not allowed" disabled /></div>
</div>usage
Usage
Every native type and attribute passes through untouched; the component only adds label/error wiring, the four slot seams, and the optional clear button.
<!-- every native type passes through untouched; every other
attribute rides through restProps (placeholder, min/max, accept…) -->
<Input type="email" label="email" placeholder="you@host.tld" required />
<Input type="password" label="api key" autocomplete="off" />
<!-- clearable adds the × in the inner-inline-end seam; value is $bindable -->
<Input type="search" label="search" clearable bind:value={q} />
<!-- count rides the native maxlength; the eye (on by default for
password) flips only the input type, starting hidden -->
<Input label="bio" count maxlength={20} bind:value={bio} />
<!-- four slot seams; the shell still owns border / hover / focus -->
<Input label="endpoint" placeholder="api.jixoai.com">
{#snippet innerInlineStart()}<span>https://</span>{/snippet}
{#snippet innerInlineEnd()}<span class="text-foreground!">/v1/spawn</span>{/snippet}
</Input>
<!-- error wiring: aria-invalid + aria-describedby + dashed shell -->
<Input type="email" label="email" value="not-an-email" error="email is required" />a11y
Accessibility
The native input keeps platform semantics; the component wires the label and the validation message to the control.
Keyboard
| Key | Action |
|---|---|
| Tab | Moves focus to the input |
| any text | Types into the field; per-type controls — the −/+ stepper and the embedded panels by default, platform spinners/pickers under native-controls |
| Esc | Browser-native search-field reset on type="search" when not cleared |
ARIA
| Attribute | Value | Description |
|---|---|---|
| aria-invalid | 'true' | Set on the native input when the error prop is provided |
| aria-describedby | {id}-error | Points at the "! message" validation line |
| aria-label | "clear value" | On the clearable × button (type="button") |
| aria-label | "show password" / "hide password" | On the reveal eye; it swaps with the state |
| aria-pressed | true/false | On the reveal eye — starts false (the value is never revealed by default); pressing flips only the input type |
| aria-live | 'off' / 'polite' | On the count readout — off until the value crosses 90% of the maxlength cap, then polite |
theming
Density and tokens
The shell, label, and error rhythm are pure density-scope tokens; resize the scope and the whole field stack follows.
the shell, label, and error rhythm are pure density-scope tokens — flip the canvas dock's density select (auto / small / medium / large — the lane vocabulary) above to re-scope the workbench stage alone; the docs chrome and every other canvas keep their seats. The four-copy DensityDemo row is retired by that select.
| Token | Default | Source |
|---|---|---|
| --jx-hit | 28 / 32 / 40 / 48px | density |
| --jx-text | 11 / 12 / 13 / 15px | density |
| --jx-line | 16 / 18 / 20 / 24px | density |
| --jx-gap | 8 / 8 / 12 / 16px | density |
| --jx-stack | 4 / 4 / 8 / 8px | density |
axes
Universal props
The eight-axis surface (explicit-props): size · shape · radius · density · color · theme · elevation · motion — each axis takes named steps, auto (inherit the ambient context; stamps nothing), an exact number (px · coefficient · dp · hue per axis), or query() for responsive/container-conditional values. The family CONSUMES size and color: the native element never receives them (the §1 native collision rule). Radius is STAMPED, NEVER CONSUMED (measured, task 104): the field stamps --jx-radius-effective while the shell hardcodes border-radius: 0 and no family sheet reads any --jx-radius-* var — the demo lane below teaches the stamp, the paint stays square by the field law (the color-picker #19 class).
input · universal props
<!-- the eight-axis surface: named steps · auto (inherit) · exact
numbers · query() for conditional values; auto stamps nothing -->
<Input label="px number" size={14} density="small" placeholder="root 14px" />
<Input label="named steps" size="large" radius="medium" placeholder="root 18px" />api
API
Props extend the native HTML input attributes; the entries below are the component-owned additions — plus the eight universal axes (the fold below per the convention) and the chrome posture. Everything else (placeholder, name, min/max, accept…) rides through restProps.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| type | string | 'text' | Any native input type; range/color/hidden switch to their own lanes. |
| label | string | — | Field label rendered as label[for] above the control. |
| error | string | — | Error text: sets aria-invalid, wires aria-describedby, dashes the shell. |
| clearable | boolean | false | Text-like only: adds the × button in the inner-inline-end seam. |
| count | boolean | false | Text-like only: the "n / max" code-point readout in the hint lane below the shell; the cap rides the native maxlength; aria-live flips off → polite from 90% of the cap. |
| reveal | boolean | true | type="password" only: the reveal eye in the end lane (outermost: snippet > clearable × > eye). Starts hidden; pressing flips only the input type between password/text. Pass false to omit. |
| label-mode | 'stacked' | 'floating' | 'stacked' | floating renders the terminal bracket — the label rides the shell's top border like a fieldset legend (recorded divergence from the SaaS in-field morph); ink states are pure CSS (empty muted / focused+filled foreground / error destructive). |
| native-controls | boolean | false | The bare attribute opts back into the platform controls: number gets its platform spinner back, and date/datetime-local/week/month/time/color open the platform popups instead of the embedded panels. |
| locale | string | page <html lang> | BCP 47 locale for the panels' vocabulary — month label, weekday heads, month cells — through Intl.DateTimeFormat (e.g. zh-CN renders 2026年8月 / 周一 / 1月). |
| picker | Snippet | — | Replaces the default embedded panel for any picker type (number is not a panel type); its ctx carries value, commit and close. |
| onselect | (value: string) => void | — | Fires when a custom panel commits a selection (date day-pick, color swatch…). |
value bind | string | number | — | Bindable; bound ⇒ controlled, absent ⇒ purely uncontrolled. |
| innerInlineStart | Snippet | — | Inside the shell, left of the lane (prefix icon / unit). |
| innerInlineEnd | Snippet | — | Inside the shell, right of the lane (suffix / unit / action). |
| icon | Snippet | null | per-type glyph | The semantic glyph, tri-state: undefined = the per-type default (url→link, tel→phone, email→mail, search→magnifier, text→the Type T — every text-like type carries one); null = explicitly OFF (embedded lanes that paint their own glyphs opt out); a snippet = the custom glyph. |
| iconPosition | 'start' | 'end' | null · ambient | Pin the semantic glyph to a side. Absent = inherit context: the css leads by default, a list-item trailing end lane rides the trailing edge, and the lane 30rem fold suspends back to leading (the inset-contract suspension precedent). |
| 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
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity across the motion kernels (§8) |