registry:ui · Data Entry
input-group — the joined field shell
The Origin UI “Input with X” family over the input's shared shell law: addons compose BESIDE the field, not inside a private composite. One 1px var(--border) bezel carries the shell states (hover lift, inset focus ring, invalid dash); each addon owns exactly one hairline seam facing the lane; the InputGroupInput stays a chromeless Tier-2 control-lane — no double borders by construction. ONE disabled propagation rule: disable the root and the lane renders native disabled while every addon goes inert (the platform's containment — buttons, selects and links inside lose activation and focus at once).
$npx jixoai-ui add input-group
https://ui.jixoai.com/r/input-group.json
the group, addon and input parts — one item
overview
Overview
Composition parts over the input's shared shell law: addons compose BESIDE the field, the group owns the one bezel and the name, and ONE disabled rule propagates.
Three parts compose the joined field: InputGroupAddon lanes (text, an
icon glyph, a PressButton, a whole NativeSelect) beside a chromeless InputGroupInput — the real <input>, keep-alive for
typing, parsing, FormData and label[for]. The group root owns ONE 1px
bezel carrying the input family's shell states (the well shadow, hover intensity,
the inset focus ring, the invalid dash), each addon owns exactly one hairline seam
facing the lane, and the input stays chromeless — no double borders by construction.
Two contracts are pinned by the family spec. The name: the root is a role=group landmark named by label (an explicit rest
aria-label wins; aria-labelledby flows through). Disabled: ONE propagation rule —
disabling the root renders the lane native-disabled while every addon goes inert
(the platform's containment), and the input's own disabled stays
per-part beside it. The house value law is avatar-plain: bound ⇒ controlled, absent
⇒ purely uncontrolled; FormData and form.reset() keep native behavior.
The eight axes resolve on the group root as a composer: density is the one CONSUMED axis (the shell atoms read the rung-re-based channels) AND the provider — the resolved tier is inherit-then-provide to the subtree (the r11 eager-capture contract, spec-pinned), so addon children adopt it. The other seven stamp-and-supply (grep receipt: zero effective-carrier readers in ui/input-group/ — the W3-era "consumes size and color" was the §1 collision rule mistaken for consumption). Theme splits by emission form: the css state machines read raw tokens and flip under a dark island; the stylex base freezes. Per-axis below; the shared grammar lives on the universal props page.
input-group
Text prefix, live value, action suffix — the playground flips the group disable and edits the prefix; the usage file in the drawer tracks both.
<InputGroup label="repository url">
<InputGroupAddon>https://</InputGroupAddon>
<InputGroupInput name="site" bind:value={site} placeholder="jixoai.com" />
<InputGroupAddon align="inline-end">
<PressButton variant="tonal">check</PressButton>
</InputGroupAddon>
</InputGroup>usage
Usage
Compose the group from parts: addons for the lanes, the input part for the field. The group root owns the name; the input owns the form participation.
<script lang="ts">
import { InputGroup, InputGroupAddon, InputGroupInput } from '@ui/input-group/index';
import PressButton from '@ui/press-button/press-button.svelte';
</script>
<InputGroup label="repository url">
<InputGroupAddon>https://</InputGroupAddon>
<InputGroupInput name="site" bind:value placeholder="jixoai.com" />
<InputGroupAddon align="inline-end">
<PressButton variant="tonal">check</PressButton>
</InputGroupAddon>
</InputGroup>examples
Examples
Ability-named demos — one phrase, one capability. Every demo is live; open the code drawer for the exact composition.
The demos below live on their own canvases — a select add-on, then the submitted-form contract. The disabled propagation rule plays in the live demo up top.
with a select add-on
A NativeSelect rides the start addon — the protocol picker — while the field keeps the host. Addons carry any control; the seam stays one hairline.
<!--
jixoai input group — the ADDON part (registry/files/ui/input-group/
input-group-addon.svelte, OpenSpec 2026-08-30-expand-form-family F2).
One add-on lane beside the group's input: prefix (align
"inline-start", the default) or suffix (align "inline-end"). The
content is the consumer's composition — muted text ("https://"),
an icon glyph (<Icon name="x" /> via $lib/ui/icon over the generated
set),
a PressButton ("search"), a NativeSelect (the unit picker) — the
addon owns ONLY the lane: flex-none rhythm over the density tokens,
muted secondary text paint, and its ONE hairline seam (the edge
facing the input; painted in input-group.css from the valued
data-jx-igroup-addon hook — the shell carries ONE border, each
addon ONE seam, the lane stays chromeless: no double borders).
Disabled propagation reads the group context: a root-level disable
renders the addon `inert` — the platform's containment, so every
interactive descendant (button, select, link) loses activation and
focus at once — plus the muted opacity paint. Per-part disables
inside the addon stay the consumer's.
-->
<script lang="ts">
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { getContext } from 'svelte';
import { INPUT_GROUP_KEY, type InputGroupApi } from './input-group.svelte';
import { inputGroupStyles } from './input-group.stylex';
// the payload's own join (the separator serialize law): every
// stylex.create member is an OBJECT in dev and the joined string in
// shipped payloads — composition goes through THIS joiner (all
// string values except $$css, space-joined).
const cx = (
...styles: ({ readonly [key: string]: string | object } | undefined | string)[]
): string =>
styles
.filter(Boolean)
.map((style) =>
typeof style === 'string'
? style
: Object.entries(style ?? {}).flatMap(([key, value]) =>
key !== '$$css' && typeof value === 'string' ? [value] : [],
).join(' '),
)
.join(' ');
interface Props extends HTMLAttributes<HTMLDivElement> {
/** which lane the addon sits in — the seam faces the input:
inline-start seams its inline-end, inline-end its inline-start */
align?: 'inline-start' | 'inline-end';
class?: string;
children: Snippet;
}
let { align = 'inline-start', class: className = '', children, ...rest }: Props = $props();
const group = getContext<InputGroupApi | undefined>(INPUT_GROUP_KEY);
const groupDisabled = $derived(group?.disabled ?? false);
</script>
<div
{...rest}
data-jx-igroup-addon={align}
inert={groupDisabled || undefined}
class={cx(
inputGroupStyles.addon,
groupDisabled && inputGroupStyles.addonDisabled,
className,
)}
>
{@render children()}
</div>
in a submitted form
The input part is a real native field: give it a name and FormData carries it. Submit empty to see the honest empty commit.
<!--
jixoai input group — the INPUT part (registry/files/ui/input-group/
input-group-input.svelte, OpenSpec 2026-08-30-expand-form-family F2).
The real <input> of the group — chromeless by construction: the
Tier-2 lane (.jx-html-control-lane, consumed-only from the shared
sheet) gives the bare field paint; the GROUP root owns the one bezel
and its state machines, the addons own the seams. No per-part
border, no focus ring, no shadow — the lane is the give-way child
(min-width 0) in a flex row of add-ons.
The house value law, verbatim: `value` is $bindable — bound ⇒
controlled, absent ⇒ purely uncontrolled (Svelte skips undefined
writes, so FormData and form.reset() keep native behavior). A
caller-supplied oninput observes alongside the internal law (never
instead of it).
Disabled propagation: the group root's `disabled` forces this input
native-disabled through the context; the input's OWN `disabled`
stays per-part (addons keep working beside a disabled lane — the
copy/check-button composition).
-->
<script lang="ts">
import type { HTMLInputAttributes } from 'svelte/elements';
import { getContext } from 'svelte';
import { INPUT_GROUP_KEY, type InputGroupApi } from './input-group.svelte';
import { inputGroupStyles } from './input-group.stylex';
// the payload's own join (the separator serialize law): every
// stylex.create member is an OBJECT in dev and the joined string in
// shipped payloads — composition goes through THIS joiner (all
// string values except $$css, space-joined).
const cx = (
...styles: ({ readonly [key: string]: string | object } | undefined | string)[]
): string =>
styles
.filter(Boolean)
.map((style) =>
typeof style === 'string'
? style
: Object.entries(style ?? {}).flatMap(([key, value]) =>
key !== '$$css' && typeof value === 'string' ? [value] : [],
).join(' '),
)
.join(' ');
interface Props extends HTMLInputAttributes {
/** $bindable; bound ⇒ controlled, absent ⇒ purely uncontrolled */
value?: string | number;
class?: string;
}
let {
value = $bindable<string | number>(),
class: className = '',
disabled,
...rest
}: Props = $props();
const group = getContext<InputGroupApi | undefined>(INPUT_GROUP_KEY);
/** the ONE propagation rule meets the per-part rule: either the
group or the input itself disables the lane */
const effectiveDisabled = $derived(disabled === true || group?.disabled === true);
const controlled = $derived(value != null);
function syncValue(event: Event) {
if (controlled) value = (event.currentTarget as HTMLInputElement).value;
// forward a caller-supplied input handler from the rest props
(rest as { oninput?: (event: Event) => void }).oninput?.(event);
}
</script>
<input
{...rest}
data-jx-igroup-input
class={cx('jx-html-control-lane', inputGroupStyles.lane, className)}
value={controlled ? value : undefined}
disabled={effectiveDisabled || undefined}
oninput={syncValue}
/>
a11y
Accessibility
The root is a named group landmark; the field keeps its native semantics; disabled containment is the platform's inert — not a theater of aria-disabled.
Keyboard
| Key | Action |
|---|---|
| Tab | Focus the field lane; addons keep their own stops (a button or select inside is a real control) |
| Disabled root | The lane is native-disabled; every addon goes inert — no focus, no activation, nothing announced as active |
ARIA
| Attribute | Value | Description |
|---|---|---|
| role | group | The root landmark — override with an explicit role through the rest props only when you own the semantics |
| aria-label | label | The group accessible name — or pass aria-labelledby through the rest props; a nameless group is announced as nothing |
| input | native | The real <input> keeps typing, parsing, FormData and label[for] wiring |
| inert | platform | Group disable contains the addons natively — descendants lose activation AND focus at once |
axes
The eight axes on input-group
A composer's surface (census batch A, the input family's shared shell law): density is the one CONSUMED axis — the shell atoms read the rung-re-based channels — and simultaneously the PROVIDER (inherit-then-provide to the subtree, the r11 eager-capture contract). The other seven stamp-and-supply with zero family readers (grep receipt; the W3-era 'consumes size and color' claim was the §1 collision rule mistaken for consumption — corrected here). Theme splits by emission form: the css state machines' raw tokens flip under a dark island while the stylex base freezes. The carriers stamp the group root, greppable in the raw SSR.
Reading the table: Property is the axis, Type is the real carrier or consumption it drives on THIS family, Default is the lane default — the named steps, number unit, and consumption are in each description.
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-size-effective; no family css reads it (grep receipt: zero readers in ui/input-group/ — the W3-era 'consumes size' claim was the §1 forwarding rule mistaken for consumption). The shell sizes from the density channels, not the size axis; the stamp supplies composed addon children. Number unit: px. |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' | SUPPLY-ONLY — stamps --jx-shape-effective and --jx-radius-factor-effective; no family css reads them (grep receipt: zero corner-shape declarations — the shell's corner comes from the consumed Tier-2 control-lane vocabulary, not the axis). Number unit: none. |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-radius-effective; no family css reads it (grep receipt: zero readers — no concentric chain lives in the shell). Number unit: px. |
| density | 'small' | 'medium' | 'large' | 'auto' | number (+ the five legacy spellings) | 'ambient scope' | CONSUMED + PROVIDED — the composer's lane. The named rung stamps data-density on the root and the scope re-bases the channels the shell atoms read: --jx-hit (the lane's min height), --jx-inset (the addon lane's padding; the input part is chromeless), --jx-text (type), --jx-gap (the addon gap) — measured below. AND the resolved tier is PROVIDED to the subtree: inherit-then-provide (the r11 eager-capture contract, spec-pinned) — addon children adopt the group's opinion. Number/query lanes carry no legacy rung (the coefficient rides the carriers). Number unit: coefficient. |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' | SUPPLY-ONLY — stamps --jx-color-effective; no family css reads it (grep receipt: zero readers — the W3-era 'consumes color' claim was the §1 forwarding rule). The shell's state voices are the raw theme tokens (--ring focus, --muted disabled); hue that paints comes from the theme or the jx-hue-* injection. Number unit: hue degrees. |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' | THE MIXED POLE — the split follows the emission form (measured below). FLIP (raw css reads in the state machines): the addon seams (--border), the focus ring tint + outline (--ring), the disabled border (--muted), and the well shadow pair (--shadow-well / --shadow-well-hover). FROZEN (stylex defineVars aliases, resolved at the :root scope): the shell ground (--jx-background), the base bezel (--jx-border on --jx-hairline) and the addon ink (--jx-muted-foreground) — one bezel, two regimes on either side. light and system stamp nothing — tree inheritance. |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-elevation-effective; no family css reads it (grep receipt: zero readers). The shell's rest/hover shadow is the input family's WELL tier recipe (--shadow-well pair) — a fixed recipe, not the §7 consumption pair. Number unit: dp. |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-motion-effective; no family css reads it. The shell's transition reads the kernel presets verbatim (--motion-150 / --motion-ease-out) with the prefers-reduced-motion kill (input-group.css) — the axis has no family-local kernel to step. Number unit: coefficient. |
Deviations, cited: the adoption is the census batch A row (the input family — openspec/changes/explicit-props/research/migration-census.md). The §1 collision rule: the native input never receives size or color ATTRIBUTES — that forwarding rule was the W3 summary's whole truth; axis CONSUMPTION is a css-read question, and the greps answer it: zero readers. The provider lane is pinned by test/defaults-form-families.spec.ts (the r11 eager-capture contract).
<!-- density: CONSUMED + PROVIDED — the shell atoms read the
rung-re-based channels, and the resolved tier is provided to the
subtree (the r11 inherit-then-provide contract) -->
<InputGroup label="endpoint" density="lg">
<InputGroupAddon>https://</InputGroupAddon>
<InputGroupInput name="host" placeholder="status.jixoai.com" />
</InputGroup>
<!-- theme: the emission-form split — the addon seams, focus ring,
disabled border and well shadow (raw css reads) flip under a
dark island; the ground and base bezel (defineVars aliases,
resolved at the stylex :root scope) hold the page profile -->
<InputGroup label="endpoint" theme="dark">
<InputGroupAddon>https://</InputGroupAddon>
<InputGroupInput name="host" placeholder="status.jixoai.com" />
</InputGroup>input-group · the consumed lane and the emission-form split
The density pair is the consumed lane, measured: the lg rung's scope re-bases --jx-hit/--jx-inset/--jx-text/--jx-gap and the shell atoms read them — the group grows in height, padding and type. The theme pair is the emission-form split, measured: the addon SEAM and the well shadow (raw css reads) follow the island while the shell's base bezel and ground (defineVars aliases resolved at the stylex :root scope) hold the page profile — the same substitution-site law heading's lead and image's panel proved, now on both sides of one bezel.
<!-- density: CONSUMED + PROVIDED — the shell atoms read the
rung-re-based channels, and the resolved tier is provided to the
subtree (the r11 inherit-then-provide contract) -->
<InputGroup label="endpoint" density="lg">
<InputGroupAddon>https://</InputGroupAddon>
<InputGroupInput name="host" placeholder="status.jixoai.com" />
</InputGroup>
<!-- theme: the emission-form split — the addon seams, focus ring,
disabled border and well shadow (raw css reads) flip under a
dark island; the ground and base bezel (defineVars aliases,
resolved at the stylex :root scope) hold the page profile -->
<InputGroup label="endpoint" theme="dark">
<InputGroupAddon>https://</InputGroupAddon>
<InputGroupInput name="host" placeholder="status.jixoai.com" />
</InputGroup><script lang="ts">
import { InputGroup, InputGroupAddon, InputGroupInput } from '@ui/input-group/index';
import { query } from '@lib/universal-props-query.svelte';
import type { DensityLane } from '@lib/defaults.svelte';
</script>
<InputGroup label="responsive group" density={query<{ md: DensityLane }, DensityLane>({ md: 'small' }, 'large')}>
<InputGroupAddon>https://</InputGroupAddon>
<InputGroupInput name="host" placeholder="status.jixoai.com" />
</InputGroup>input-group · query()
Media keys are min-width: below 48rem the base applies — the large rung, the touch-size shell; at 48rem and wider the md case wins and the group compacts. The string lane takes both generics. Resize across 48rem.
<script lang="ts">
import { InputGroup, InputGroupAddon, InputGroupInput } from '@ui/input-group/index';
import { query } from '@lib/universal-props-query.svelte';
import type { DensityLane } from '@lib/defaults.svelte';
</script>
<InputGroup label="responsive group" density={query<{ md: DensityLane }, DensityLane>({ md: 'small' }, 'large')}>
<InputGroupAddon>https://</InputGroupAddon>
<InputGroupInput name="host" placeholder="status.jixoai.com" />
</InputGroup>| Token | Default | Source |
|---|---|---|
| --jx-hit | 28 / 32 / 40 / 48px | density |
| --jx-inset | 8 / 8 / 12 / 16px | density |
| --jx-text | 11 / 12 / 13 / 15px | density |
| --jx-gap | rung gap | density |
| --shadow-well / -hover | the input family's well tier | structural |
| --ring / --muted | raw theme tokens | structural |
| --border seams | raw theme token | structural |
| --jx-background / --jx-border / --jx-muted-foreground | defineVars aliases (frozen) | structural |
| --jx-hairline | 1px | structural |
api
API
Three parts, one context: the root owns the disable propagation and the name; the addon owns a lane; the input owns the field. The table renders 17 meta entries − 2 citation duplicates = 15 unique − 8 axis-named = 7 family rows + the synthesized rest (with data-density as a family row — the legacy attribute escape hatch) = 8 served rows.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| label | string | — | The group accessible name (aria-label shorthand) — a nameless group is announced as nothing; an explicit rest aria-label wins; aria-labelledby flows through the rest props. |
| disabled | boolean | false | THE ONE propagation rule: native disabled on the lane + inert on EVERY addon (the platform's containment — activation and focus lost at once). The input part's own disabled stays per-part beside it. |
| data-density | string | — | The legacy attribute-name escape hatch, forwarded verbatim — the universal density lane above is the documented surface (inherit-then-provide). |
| role | string | 'group' | The group landmark (default 'group') — override only when you own the semantics. |
| class | string | '' | Merged into the root shell (cn()). |
| children | Snippet | — | The parts, authored in your tree: InputGroupAddon lanes beside a chromeless InputGroupInput — composition, not configuration. |
| aria-label | unknown | — | |
| rest | unknown (spread passthrough) | — | aria-labelledby, data-*, event handlers — land on the root verbatim (the group landmark's attribute surface). |
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) |
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| align (InputGroupAddon) | 'inline-start' | 'inline-end' | 'inline-start' | Which lane the addon sits in; the hairline seam always faces the input. |
| children (InputGroupAddon) * | Snippet | required | Text, an icon glyph, a PressButton, a NativeSelect — your composition. |
value (InputGroupInput) bind | string | number | — | Bound ⇒ controlled, absent ⇒ purely uncontrolled (FormData / form.reset untouched). |
| disabled (InputGroupInput) | boolean | false | Per-part lane disable — addons keep working beside it. |
| ...rest (InputGroupInput) | HTMLInputAttributes | — | name, placeholder, type, required — every native input attribute. |
see also
See also
The family around the joined shell.