Swap#

A boolean with two faces. On and Off both stay rendered — the component exists for the CSS transition between them — and the inactive face is aria-hidden. By default a swap is a display element, a <span> that follows external state; with interactive it becomes a real <button aria-pressed> that toggles on click.

Import#

TSX
import { Swap } from '@sigx/zero/swap';

Swap is a compound: Swap.Root, Swap.On, Swap.Off. It is also re-exported from the @sigx/zero root, together with swapAnatomy and useSwapContext.

Usage#

TSX
import { component } from 'sigx';
import { Swap } from '@sigx/zero/swap';

const ThemeIcon = component(({ signal }) => {
    const state = signal({ dark: false });

    return () => (
        <Swap.Root model={() => state.dark}>
            <Swap.On><MoonIcon /></Swap.On>
            <Swap.Off><SunIcon /></Swap.Off>
        </Swap.Root>
    );
});

model={() => state.dark} binds the boolean both ways. Leave the model off and pass defaultOn to keep the state inside the component; change fires either way. See Models.

Rendered like this the swap has no semantics of its own: it is an icon that follows state.dark, and whatever sets state.dark — a switch elsewhere, a media query — is the control.

Interactive#

TSX
<Swap.Root interactive label="Toggle dark mode" model={() => state.dark}>
    <Swap.On><MoonIcon /></Swap.On>
    <Swap.Off><SunIcon /></Swap.Off>
</Swap.Root>

interactive makes the swap a control: the root renders a native <button> with aria-pressed mirroring the model, a click toggles it, and the platform supplies Enter and Space. The faces are usually glyphs, so label gives the button its accessible name. Interactive is opt-in on purpose — a display element must not claim button semantics.

Disabled#

TSX
<Swap.Root interactive disabled label="Toggle dark mode" model={() => state.dark}>

disabled disables the button and renders data-disabled. On a display swap the attribute still renders — a swap that cannot be operated is a styling fact, so the faces fade like the button form's would.

Anatomy#

PartElementStatesFlagsNotes
rootspanon | offdisabled, focus-visible, pressed, press-animatingA button with aria-pressed and aria-label under interactive. Carries the variant axes. Publishes press feedback when interactive.
onspanon | offThe face shown while on; aria-hidden while off. Inside root.
offspanon | offThe face shown while off; aria-hidden while on. Inside root.

Every part carries data-scope="swap" and data-part="<part>". All three parts carry the same on / off state: the faces need it to style their own presence, the root to host the transition. Neither face is ever hidden — the attribute computes display: none, which would kill the cross-fade — so the inactive face is painted for the animation and absent for assistive technology. There is no indeterminate face: the model is a boolean, and a third face would be a third state wearing a styling costume. See The anatomy contract.

Props#

Swap.Root#

PropTypeDefaultDescription
modelbooleanTwo-way binding of the on / off state.
defaultOnbooleanfalseInitial state when uncontrolled.
changeevent (on: boolean)Fires whenever the state changes.
interactivebooleanfalseRender a <button aria-pressed> that toggles on click.
labelstringAccessible name for the interactive form (aria-label).
disabledbooleanfalseInert when interactive; renders data-disabled in both forms.
color / size / variant / axes / modsdesign-system vocabularyThe variant axes, rendered as data-* on root.
classstringExtra classes on the root element.

Swap.On, Swap.Off#

Only class.

Keyboard#

Only under interactive, where the root is a native button:

KeyAction
Enter / SpaceToggle the state.
TabThe button is one tab stop.

A display swap is not focusable and has no keyboard.

In the shipped design systems#

Both @sigx/zero-basic and @sigx/zero-daisyui wire color (the eight recommended roles) and size (xsxl) on the swap root, so <Swap.Root color="primary" size="lg"> is styled in both. Neither wires a variant or any mods on the scope; under a design system's /register import those props are therefore absent. See Typed vocabulary.

The recipe stacks the two faces in the same grid cell and keys the transition on data-state: the face whose part matches the state at full opacity, the other at zero. Rotate and flip looks are recipe styling on the same two states, not contract. The faces carry no text token hint on purpose — they are glyph slots, and the inactive one is legitimately painted at opacity 0, which is exactly what a text-legibility check must not be asked to read.

Toggle for a pressed-mode button with one face, Switch for the form control.