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

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).

all native types 4 slot seams clearable count · reveal · floating label[for] + aria wiring zero deps · Svelte 5 runes

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-usage.svelte svelte
<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

September 2026
MonTueWedThuFriSatSun
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
September 2026
MonTueWedThuFriSatSun
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
September 2026
MonTueWedThuFriSatSun
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2026
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
input-all-types-demo.svelte svelte
<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:

.jx-control · .jx-slider · .jx-color-shell + .jx-color-swatch — compact 7:4 chip by default, .jx-color-expand reclaims the full row; the wrapper label opens the picker from the pipette zone too
Tier-1 · bare markup
<!-- 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).

https:// /v1/spawn
outer-block-end — helper text below the shell; an error line would render above it
$ per seat / mo

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.

slots
<!-- 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

code-point readout — 你好𠀀👍 counts 4; aria-live flips off → polite from 90% of the cap

0

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 · reveal · floating
<!-- 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

error
<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).

September 2026
MonTueWedThuFriSatSun
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
September 2026
MonTueWedThuFriSatSun
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2026
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
September 2026
MonTueWedThuFriSatSun
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2026年9月
周一周二周三周四周五周六周日
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

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.

picker-bridge
<!-- 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

input-types-demo.svelte svelte
<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.

Input usage
<!-- 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

KeyAction
TabMoves focus to the input
any textTypes into the field; per-type controls — the −/+ stepper and the embedded panels by default, platform spinners/pickers under native-controls
EscBrowser-native search-field reset on type="search" when not cleared

ARIA

AttributeValueDescription
aria-invalid'true'Set on the native input when the error prop is provided
aria-describedby{id}-errorPoints 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-pressedtrue/falseOn 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.

TokenDefaultSource
--jx-hit28 / 32 / 40 / 48pxdensity
--jx-text11 / 12 / 13 / 15pxdensity
--jx-line16 / 18 / 20 / 24pxdensity
--jx-gap8 / 8 / 12 / 16pxdensity
--jx-stack4 / 4 / 8 / 8pxdensity

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

input-universal.svelte svelte
<!-- 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

PropertyTypeDefaultDescription
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 booleanfalseText-like only: adds the × button in the inner-inline-end seam.
count booleanfalseText-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 booleantruetype="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 booleanfalseThe 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 stringpage <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 bindstring | 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 | nullper-type glyphThe 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 · ambientPin 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

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)
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h