Switch
A setting that is on or off, and takes effect the moment it is flipped.
Overview
A setting that is on or off, and takes effect the moment it is flipped.
First example
<Switch isSelected={isOn} onSelectedChange={setIsOn}>Mode sombre</Switch><Switch defaultSelected onSelectedChange={save}>Synchroniser en Wi-Fi seulement</Switch>
Anatomy
<Switch>
<Switch.Track>
<Switch.Thumb />
</Switch.Track>
<Switch.Label />
</Switch>
Switch— the root, and it is the row, not the track. It is the pressable: it owns the state, resolves the recipe once (R5) and publishes it to its slots.Switch.Track— the bar the knob slides on, and the surface that says on or off.Switch.Thumb— the knob. The only thing on this component that moves.Switch.Label— what the setting is.
The root is the row so that tapping the label flips the switch — the Checkbox's
shape, for the Checkbox's reason.
R3 — and the track comes with the auto-wrap:
<Switch>Mode sombre</Switch>
// is exactly
<Switch>
<Switch.Track>
<Switch.Thumb />
</Switch.Track>
<Switch.Label>Mode sombre</Switch.Label>
</Switch>
With no children at all it is the track alone — the form a settings list wants, where the label is the row's own text.
Usage
Controlled, or not
<Switch isSelected={isOn} onSelectedChange={setIsOn}>
Mode sombre
</Switch>
<Switch defaultSelected onSelectedChange={save}>
Synchroniser en Wi-Fi seulement
</Switch>
onPress still fires, composed with the flip.
The two shapes
<Switch variant="primary">Le pouce à l'intérieur</Switch>
<Switch variant="secondary">Le pouce par-dessus</Switch>
variant is a geometry axis on this component, which is unusual and deliberate — both
shapes are the accent when they are on, and what changes is where the knob sits:
primary(the default) rides the thumb inside the track, clear of its edges by the track's own inset. The shape everything else in this library shares: a filled surface with something on it.secondarymakes the track a thin bar and stands the thumb over it, overhanging above and below. Less ink, and the knob reads as the thing you drag.
They are the legacy component's inside and overlap, under the library's own two names —
the same two shapes, the same measurements, and one vocabulary across the whole v1 API
instead of a third pair of words for this component alone.
A glyph on the knob
<Switch.Track>
<Switch.Thumb>
<Icon as={CheckIcon} size={12} color={theme.colors.accent} />
</Switch.Thumb>
</Switch.Track>
The knob's children travel with it. Anything else you put in the track stays where you put
it — a glyph at each end, for instance, which is what the reference implementation's StartContent and
EndContent are; here they are two Views you position, because the track is a node you
were given rather than one that was hidden from you.
Disabled
<Switch isDisabled defaultSelected>
Géré par votre organisation
</Switch>
On the row, not the track: what is disabled is the control, and the label is part of it.
As another element
<Switch asChild isSelected={isOn} onSelectedChange={setIsOn}>
<Animated.View layout={LinearTransition}>…</Animated.View>
</Switch>
R12 — the caller's element is the row, so it takes the children it was written with and the auto-wrap does not apply.
Root props
SwitchProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| variant | SwitchVariant | undefined | No | — |
| size | SwitchSize | undefined | No | The track, the thumb, the gap and the label's type. |
| radius | RadiusKey | undefined | No | Overrides the track's corner, which is `full`. The thumb stays round. |
| color | string | undefined | No | A raw tint (`'#7c3aed'`), never a token (R7): the colour the track takes once the switch is on, with a thumb derived to read against it. The track at rest keeps the neutral it had — a switch that is off is off in every brand. |
| isSelected | boolean | undefined | No | Controlled. Leave it out and the switch keeps its own state. |
| defaultSelected | boolean | undefined | No | The starting value when uncontrolled. |
| onSelectedChange | ((isSelected: boolean) => void) | undefined | No | Fired with the new value on every flip, controlled or not. |
| isDisabled | boolean | undefined | No | Dims the row and stops the press. |
| animation | AnimationProp | undefined | No | — |
| style | StyleProp<ViewStyle> | ((state: PressableStateCallbackType) => StyleProp<ViewStyle>) | No | R9 — `Pressable`'s function form as much as an object or an array. |
| children | ReactNode | No | — |
Slots
SwitchLabelProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
SwitchThumbProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | A glyph on the knob — a check, a moon. It travels with it. |
| animation | boolean | undefined | No | `false` puts the thumb at its end of the track with no slide. |
| style | StyleProp<ViewStyle> | No | — |
SwitchTrackProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | The thumb, and anything else the caller lays over the track. |
| animation | boolean | undefined | No | `false` swaps the colour at the flip instead of crossing to it. |
| style | StyleProp<ViewStyle> | No | — |
Variants, sizes and colour
Sizes
primary — the knob inside the track:
size | Track | Knob | Inset | Travel | Label |
|---|---|---|---|---|---|
sm | 44 × 26 | 20 | 3 | 18 | 14/20 |
md | 48 × 28 | 22 | 3 | 20 | 16/24 |
lg | 56 × 32 | 26 | 3 | 24 | 18/28 |
secondary — the knob over the bar:
size | Track | Knob | Inset | Travel | Label |
|---|---|---|---|---|---|
sm | 44 × 18 | 24 | 0 | 20 | 14/20 |
md | 48 × 18 | 26 | 0 | 22 | 16/24 |
lg | 56 × 20 | 30 | 0 | 26 | 18/28 |
md is the legacy switch measured — a 48 × 28 track with a 22 knob — and the reference implementation's is the
same 48 wide.
The width is part of the control, unlike everywhere else in the library, where size
never touches it: a switch is a fixed shape, and a track that stretched with its parent
would be a progress bar.
The travel is not a number anyone writes: it is width − knob − 2 × inset, arithmetic the
root does on the flattened style, because a slide happens in a worklet and a worklet needs
a number rather than a style to flatten every frame.
radius is the track's corner, full by default. The knob stays round — a squared
track with a round knob is a real design; the reverse is not.
Colour
<Switch color="#7c3aed">Teinté</Switch>
A raw tint (R7), and here it is the colour the switch turns on to, with a knob derived to read against it. The track at rest keeps its neutral: a switch that is off is off in every brand.
It reaches there through the bgSelected / fgSelected roles, which the tint pass
repaints — the same mechanism as the Checkbox's, written up once in
checkbox.md.
The knob at rest is the theme's white, one of the two places in the library where a
primitive is named on purpose: a switch's knob is white in both modes, the way the
platform's own is, because it has to read against the neutral track and against the
accent one.
Accessibility
accessibilityRole="switch"on the root, overridable, withaccessibilityState.checkedfollowing the value and.disabledfollowingisDisabled. A caller's ownaccessibilityStateis merged, not spread over.- The label is inside the control, so the switch announces itself with its text. A
switch with no label needs an
accessibilityLabel. - The whole row is the touch target.
- The label does not change with the state. "Mode sombre" is the setting whether it is on or off; a label reading "Activé" would say what the track and the role already say, twice.
Migration from legacy
| Legacy | v1 |
|---|---|
label="…" | <Switch>…</Switch> — the text child is the label |
labelAlignment="left" | write <Switch.Label> before <Switch.Track> |
labelAlignment="justify-*" | justifyContent="space-between" plus a width, in style props |
variant="inside" | variant="primary" — the same shape, the library's name |
variant="overlap" | variant="secondary" — likewise |
isSelected / onValueChange | isSelected / onSelectedChange, plus defaultSelected |
themeColor="primary" | color={theme.colors.accent} — a raw value (R7) |
size="sm" | "md" | "lg" | size — the same three |
radius | radius — a RadiusKey now, still full by default |
fullWidth | width="100%" in style props (R14) |
isDisabled | unchanged, on the root |
labelStyle | style on <Switch.Label> |
style | style on the root |
Implementation notes
A switch is not a checkbox
They look like the same control with two skins and they are not:
Checkbox | Switch | |
|---|---|---|
| When it acts | Later, when the form is submitted | Immediately |
| What it says | An intention | A state of the system |
isInvalid | Yes — the value can be wrong | No |
| Indeterminate | Yes — a "select all" needs it | No |
There is no isInvalid here because a setting that has already taken effect has no later
moment at which it can be wrong. A setting that cannot be turned on is isDisabled, with
the reason written beside it.
Animation
The track's colour is crossed, not swapped — interpolateColor between the two ends —
and the knob slides on the same 175ms, so a flip reads as one movement rather than as a
repaint under a jump. The number lives in switch.style.ts and neither slot owns it, which
is what keeps them arriving together.
animation={false} on Switch.Track or Switch.Thumb puts that one at its end state with
no worklet mounted at all. animation on the root is the library's usual press knob —
false, 'disabled', 'disable-all', or the object — and belongs to PressableFeedback.
The knob moves with translateX, not with an edge. R13 bans a directional inset, and a
transform is not one — but a transform does not mirror under RTL either, so the sign is
flipped by hand against I18nManager.isRTL. It is the one place in the library that reads
it, and it reads it for a movement rather than for a layout.
Alignment with the reference implementation
Measured against their switch.tsx and switch.css, and against the legacy component this
one replaces.
Identical to the reference implementation: the 48pt track at md, the knob's field shadow, the accent
track when on with a contrasting knob, the 175ms crossfade, and a default knob so nothing
has to be written for the common case.
Four deltas:
| Theirs | Ours | Why |
|---|---|---|
| The root is the track; no label | The root is the row, Label is a slot | The Checkbox's reason, and the legacy component had a label too. |
| A 28 × 20 pill knob | A round knob, 22 at md | The legacy switch's, and the platform's. A pill knob in a round track is a shape neither of our two variants is. |
| No shape variants | primary / secondary | The legacy component shipped both, and dropping one at the migration would be a redesign rather than a port. |
StartContent / EndContent | Children of the track | The track is a slot here, so a glyph at either end is a View you place — one API instead of two named positions. |
Extending it
useSwitch() is exported (R10) and carries the resolved styles, the two colour pairs, the
travel and the state — enough to write a glyph that changes at each end of the track, or a
second label:
import { useSwitch } from '@xaui/native/switch'
function SwitchState() {
const { isSelected, labelStyle } = useSwitch()
return <Text style={labelStyle}>{isSelected ? 'Activé' : 'Désactivé'}</Text>
}
Outside a <Switch> it throws by name.