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

registry:ui · General

transfer — two fieldsets and a batch mover

The two-panel selector the ruled way: each side is a real fieldset of real checkbox rows — grouping, labeling and toggling all native. The middle buttons batch-move every checked row at once, then the selection clears (checked is a transient moving state, never the value). Per-panel search filters its own list; disabled rows render but never move. value is the TARGET list — what sits on the right is the answer.

install — transfer

$npx jixoai-ui add transfer

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

fieldset + checkbox rows batch move · selection clears per-panel search value = target list one-way · select-all recipes

overview

Overview

The two-panel selector: each side is a real fieldset of real checkbox rows (grouping, labeling and toggling all native), the middle buttons batch-move every checked row, and value — the bindable TARGET list — is the answer.

The state model is three facts. ONE: placement derives — the panels are complements of the same option set (source = not in value, target = in value), so an option lives on exactly one side at every instant; there is no transit state where a key exists in both lists. TWO: moves are batch and atomic — every checked row crosses in one value assignment, then the checkbox selection clears (checked is a transient moving state, never the value). THREE: disabled rows render but never move; the movers disable themselves when nothing movable is checked.

Both lists render from keyed each blocks keyed on option.value — with the complement partition above, a key is unique across the UNION of the two lists, not just per list (the LAW #18 double-weight surface, measured on the served DOM: mounted-children census after every move class). Per-panel search filters its own list by label substring; the legends are footers of truth ("n/total visible"), and a name wires the target values into FormData as multi-entries through the jx-form-field bridge.

The eight axes: a FIRST-TIME NO-OWN container surface — density is the ONE consumed axis (the rows read the --jx-text kernel lane; measured rung flip), theme rides the :root-pinned alias inks (frozen under a scoped island, re-derived at root-level dark) with the hover/focus leans on the scope-re-derived base tokens, and size/shape/radius/color/elevation/motion carry the supply chain (zero effective readers, grep receipts; the family is motionless by design — no transition declarations). Kinship: checkbox (the row primitive), table (the selection recipe suite), tags-input (the other keyed list-shuttle, one field wide).

transfer

Check rows on either side, then fire the middle mover — every checked row crosses at once and the selection clears. The echo footer shows the target list.

source · 3/3 visible
target · 1/1 visible
transfer-usage.svelte svelte
<Transfer {options} value={["keep"]} />

usage

Usage

value is the target list — what sits on the right is the answer; checked is a transient moving state, never the value.

Transfer usage
<script lang="ts">
  import Transfer from '@ui/transfer.svelte';
</script>

const options = [
  { value: 'a', label: 'alpha' },
  { value: 'b', label: 'beta' },
];

<Transfer {options} bind:value />

examples

Examples

Ability-named recipes over the public value/onchange seam — the component ships neither oneWay nor a header select-all, so both are compositions here.

Both recipes page-own the state machine; the component gaps (a native oneWay with a checkbox-free target panel, an in-header select-all with custom labels) are recorded in the change's followups.md.

with one-way moves

A provisioning flow that only ever grants: check a runtime on the left, fire the mover, it lands on the right — now check it on the right and fire the return mover: nothing happens, the rejection counter ticks. The committed list never shrinks.

available · 5/5 visible
assigned · 1/1 visible

rejected removals: 0

transfer-one-way-usage.svelte svelte
<script lang="ts">
  // oneWay, composed: additions commit, removals bounce — the
  // committed list never shrinks. (A native oneWay prop — checkbox-free
  // target panel — is the recorded followup.)
  let committed = $state(['node']);
  let rejected = $state(0);
  function oneWayGuard(next: string[]) {
    if (next.length >= committed.length) committed = next;
    else rejected += 1;
  }
</script>

<Transfer {options} value={committed} onchange={oneWayGuard}
  sourceTitle="available" targetTitle="assigned" />
<p aria-live="polite">rejected removals: {rejected}</p>

with batch select-all

One press grants every enabled scope at once — the external batch control writes the whole value, the panel label tracks the committed count, and the readout lists what the labels resolve to. admin is disabled and never crosses.

available · 4/4 visible
granted · 1 · 1/1 visible

granted: read access

transfer-select-all-usage.svelte svelte
<script lang="ts">
  // batch select-all, composed: one press commits every ENABLED option;
  // the panel labels track the committed list. (An in-header select-all
  // checkbox + selectAllLabels prop is the recorded followup.)
  let granted = $state(['read']);
  const grantedLabels = $derived(
    granted.map((value) => options.find((option) => option.value === value)?.label ?? value));
</script>

<div class="flex flex-wrap gap-2">
  <PressButton variant="outline"
    onclick={() => (granted = options.filter((o) => !o.disabled).map((o) => o.value))}>
    select all → move
  </PressButton>
  <PressButton variant="ghost" onclick={() => (granted = [])}>return all</PressButton>
</div>

<Transfer {options} bind:value={granted}
  sourceTitle="available" targetTitle={'granted · ' + granted.length} />
<p aria-live="polite">granted: {grantedLabels.join(', ') || '—'}</p>

types

Transfer variants

Plain source/target panels by default; titled panels rename the fieldsets, and disabled rows render but never move.

transfer · variants

source · 3/3 visible
target · 0/0 visible
  • nothing here yet
available · 2/2 visible
chosen · 1/1 visible
transfer-types-demo.svelte svelte
<script lang="ts">
  import Transfer from '@ui/transfer.svelte';
</script>

const typesPlain = [
  { value: 'html', label: 'html' },
  { value: 'css', label: 'css' },
  { value: 'js', label: 'js' },
];
const typesTitled = [
  { value: 'draft', label: 'draft post', disabled: true },
  { value: 'review', label: 'in review' },
  { value: 'done', label: 'published' },
];
let typesPlainValue = $state<string[]>([]);
let typesTitledValue = $state<string[]>(['done']);

<div class="grid w-full items-start gap-4 min-[900px]:grid-cols-2">
  <div class="border border-border p-4"><Transfer options={typesPlain} bind:value={typesPlainValue} /></div>
  <div class="border border-border p-4"><Transfer options={typesTitled} bind:value={typesTitledValue} sourceTitle="available" targetTitle="chosen" /></div>
</div>

theming

Density and tokens

Rows, movers and search lanes paint through theme colors; the panels stack under a 480px container query.

xs
source · 3/3 visible
target · 0/0 visible
  • nothing here yet
sm
source · 3/3 visible
target · 0/0 visible
  • nothing here yet
default
source · 3/3 visible
target · 0/0 visible
  • nothing here yet
lg
source · 3/3 visible
target · 0/0 visible
  • nothing here yet
TokenDefaultSource
--jx-scrollbar-thinstable guttercomponent
panel surfacevar(--card) + shadow-2xscolor
hover / focus--muted / --ring / --primarycolor
stacking lawmax-width 480pxstructural
--jx-hit28 / 32 / 40 / 48pxdensity

api

API

The target list binds both ways; a name wires the values into FormData through the jx-form-field bridge.

Properties

PropertyTypeDefaultDescription
options * TransferOption[]—The full option set; placement derives from value.
value bindstring[][]Values living on the TARGET side.
name string—Form field name — target values submit as multi-entry FormData.
sourceTitle string'source'Source fieldset legend.
targetTitle string'target'Target fieldset legend (the recipes derive it from committed state).
searchPlaceholder string'filter…'Search lane placeholder.
onchange (value: string[]) => void—Fires after each batch move with the new target list — the oneWay recipe guards through it.
class string''Extra classes on the root.

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)

TransferOption

PropertyTypeDefaultDescription
value * string—The submit value.
label * string—Row label.
disabled boolean—Row renders but never moves.

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 dual-list composite is a FIRST-TIME no-own container surface — one number moves both panes; the fieldsets, search inputs and mover chips are family parts riding the ambient chain.

PropertyTypeDefaultDescription
density '2xs' | 'xs' | 'sm' | 'default' | 'lg' | 'auto' | number (+ the five legacy spellings)'auto'THE ONE CONSUMED AXIS — the rows read the --jx-text kernel lane (coefficient-scaled), so a rung stamp rescales every option row; data-density lands on the root (in-flow — no portal, the scope covers both panels). The legend/search chrome rides static label steps. Measured: the row font flips across rungs. Number unit: coefficient.
size 'small' | 'medium' | 'large' | 'auto' | number'auto'SUPPLY-ONLY for the family's paint — zero --jx-size-effective readers over ui/transfer/ (grep receipt); the carrier's font-size lands on the root, so composed recipe content scales while the rows stay pinned to the kernel/text tokens. Number unit: px.
shape 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto'SUPPLY-ONLY — zero --jx-shape-effective readers (grep receipt). Number unit: none.
radius 'small' | 'medium' | 'large' | 'auto' | number'auto'SUPPLY-ONLY — the panels keep the static --jx-radius token corner; zero --jx-radius-effective readers over ui/transfer/ (grep receipt). Number unit: px.
color 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto'SUPPLY-ONLY — zero --jx-color-effective readers (grep receipt); the ink is the pinned token set (panels --jx-card/--jx-border, rows --jx-foreground, the mover hover lean --primary). Number unit: hue degrees.
theme 'light' | 'dark' | 'system' | 'auto''auto'THE ALIAS-FROZEN GROUND + SCOPE-RE-DERIVED LEANS (measured, probe): panels/rows/search paint through :root-pinned alias tokens (--jx-card/--jx-border/--jx-foreground/--jx-background — frozen under a scoped island, re-derived at root-level dark, stratum #2), while the hover/focus leans ride the base tokens (--muted/--ring/--primary) that the .dark scopes re-declare (stratum #3) — an island flips the leans but holds the grounds. class:dark stamps the in-flow ROOT, so the island covers both panels — no portal severance here. No number lane.
elevation 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto'SUPPLY-ONLY — zero elevation-carrier readers (grep receipt); the panels keep the static --jx-shadow-2xs chip shadow. Number unit: dp.
motion 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto'MOTIONLESS BY DESIGN — zero transition declarations in the family css and zero --jx-motion-effective readers (grep receipts); the hover/focus poses flip instantly (native checkbox semantics need no tween). Number unit: coefficient.

Receipts: the LAW #18 double weight (both lists keyed option.value; the complement partition makes each key unique across the UNION of the two lists — an option lives on exactly one side at every instant; moves are ONE atomic value assignment, so there is no transit state where a key exists in both lists; mounted-children census matched the value list after every move class — single move, batch move, return move — probe, task 46), the mutation-diff discipline (+1 per single move, +N per batch, −N per return, selection cleared after each), the search filter (per panel, label substring, case-insensitive — the legends track visible/total), the disabled-row law (renders, checks nothing, never crosses; the movers disable at zero movable), the FormData bridge (a name submits the target values as multi-entries) and LAW #19 (duplicate ids NONE page-wide) were measured on this page's served DOM (probe, task 46). The theme split (alias-frozen grounds + scope-re-derived leans — the measured island seat) and the density rung flip (the ONE consumed axis) close the battery. One capture receipt: the served demos sit inside component-canvas stages whose data-theme="light" pin (the HOST stratum — attribute-scoped re-declaration, distinct from the token strata #2/#3 above) holds the ground for its subtree — under root-level dark the --jx-card alias re-derives at :root (1.0 → 0.3211, measured) while a pinned stage's panel holds the light value.

Transfer · universal props

source · 4/4 visible
target · 0/0 visible
  • nothing here yet
source · 4/4 visible
target · 0/0 visible
  • nothing here yet
source · 4/4 visible
target · 0/0 visible
  • nothing here yet
transfer-universal.svelte svelte
<Transfer {options} size={18} density="small" theme="dark" />

a11y

Accessibility

Grouping, labeling and toggling are all native — each panel is a real fieldset of real checkbox rows.

Keyboard

KeyAction
TabWalk the fieldsets, checkbox rows, search lanes and mover buttons
SpaceToggle the focused checkbox row (native input)

ARIA

AttributeValueDescription
fieldset / legendnativeEach panel is a real fieldset; the legend shows visible/total counts.
aria-label (movers)move selected to {side}Names each middle mover button.
aria-label (search)filter {panel}Names each per-panel search lane.
aria-livepolite (recipe)The one-way rejection counter and granted readout announce without stealing focus.
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h