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

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

install — input-group

$npx jixoai-ui add input-group

https://ui.jixoai.com/r/input-group.json

the group, addon and input parts — one item

composition parts 1px hairline seams one disabled rule · inert Tier-2 control-lane zero deps · Svelte 5 runes

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.

https://
input-group-usage.svelte svelte
<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.

Input group usage
<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.

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

https://
.jixoai.app
input-group-input.svelte svelte
<!--
  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

KeyAction
TabFocus the field lane; addons keep their own stops (a button or select inside is a real control)
Disabled rootThe lane is native-disabled; every addon goes inert — no focus, no activation, nothing announced as active

ARIA

AttributeValueDescription
rolegroupThe root landmark — override with an explicit role through the rest props only when you own the semantics
aria-labellabelThe group accessible name — or pass aria-labelledby through the rest props; a nameless group is announced as nothing
inputnativeThe real <input> keeps typing, parsing, FormData and label[for] wiring
inertplatformGroup 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.

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

the eight axes on input-group
<!-- 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

density auto — the ambient rung
https://
density="lg" — the shell breathes (hit, inset, text, gap)
https://
light — the ambient profile
https://
theme="dark" — the state machines and seams flip; the stylex base freezes
https://

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.

input-group-axes.svelte svelte
<!-- 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>
one real query() case
<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()

https://

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.

input-group-query-demo.svelte svelte
<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>
TokenDefaultSource
--jx-hit28 / 32 / 40 / 48pxdensity
--jx-inset8 / 8 / 12 / 16pxdensity
--jx-text11 / 12 / 13 / 15pxdensity
--jx-gaprung gapdensity
--shadow-well / -hoverthe input family's well tierstructural
--ring / --mutedraw theme tokensstructural
--border seamsraw theme tokenstructural
--jx-background / --jx-border / --jx-muted-foregrounddefineVars aliases (frozen)structural
--jx-hairline1pxstructural

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

PropertyTypeDefaultDescription
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 booleanfalseTHE 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

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)

Properties

PropertyTypeDefaultDescription
align (InputGroupAddon) 'inline-start' | 'inline-end''inline-start'Which lane the addon sits in; the hairline seam always faces the input.
children (InputGroupAddon) * SnippetrequiredText, an icon glyph, a PressButton, a NativeSelect — your composition.
value (InputGroupInput) bindstring | number—Bound ⇒ controlled, absent ⇒ purely uncontrolled (FormData / form.reset untouched).
disabled (InputGroupInput) booleanfalsePer-part lane disable — addons keep working beside it.
...rest (InputGroupInput) HTMLInputAttributes—name, placeholder, type, required — every native input attribute.
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h