Switch

An on/off control with a thumb you can drag as well as tap.

import { Switch } from "@delacour/native-ui/switch";
Tap or drag
import { SWITCH_SIZES, Switch } from "@delacour/native-ui/switch";import type { ReactElement } from "react";import { View } from "react-native";export function Demo(): ReactElement {	return (		<View className="flex-row items-center gap-4">			{SWITCH_SIZES.map((size) => (				<Switch color="primary" defaultSelected key={size} size={size} testID={`switch-${size}`} />			))}		</View>	);}

One Gesture.Pan() drives the whole control. A tap toggles it; a drag takes the thumb with your finger, and letting go settles by position — or by the flick's velocity if you release fast. Drag one half way and back and it commits nothing.

Leave isSelected off and the switch holds its own state; defaultSelected sets the starting point.

Colours

Colours
Colours
import { SWITCH_COLORS, Switch } from "@delacour/native-ui/switch";import { Text } from "@delacour/native-ui/text";import { type ReactElement, useState } from "react";import { View } from "react-native";/** * One colour, on and off, side by side. * * Built from the exported `as const` array rather than written out, so a colour * added to `SWITCH_COLORS` appears here with no edit. */function ColorRow({ color }: { color: (typeof SWITCH_COLORS)[number] }): ReactElement {	const [isSelected, setSelected] = useState(true);	return (		<View className="flex-row items-center gap-4">			<Switch color={color} isSelected={isSelected} onSelectedChange={setSelected} />			<Switch color={color} defaultSelected={false} />			<Text.Caption>{color}</Text.Caption>		</View>	);}export function Demo(): ReactElement {	return (		<View className="gap-3">			{SWITCH_COLORS.map((color) => (				<ColorRow color={color} key={color} />			))}		</View>	);}

default, primary, success, warning, danger, info — Badge's, Checkbox's and Slider's set. An off switch is the same chrome at all six: the colour only says what being on means. Both the track and the knob fade between two token values, driven by the thumb's own travel.

Sizes

Sizes
Sizes
import { SWITCH_SIZES, Switch } from "@delacour/native-ui/switch";import { Text } from "@delacour/native-ui/text";import type { ReactElement } from "react";import { View } from "react-native";export function Demo(): ReactElement {	return (		<View className="gap-3">			{SWITCH_SIZES.map((size) => (				<View className="flex-row items-center gap-4" key={size}>					<Switch color="success" defaultSelected size={size} />					<Switch color="success" size={size} />					<Text.Caption>size {size}</Text.Caption>				</View>			))}		</View>	);}

sm, md, lg. The knob is a rounded rectangle lying on its side, not a disc — as wide as the track is tall, and shorter than it by twice the vertical inset. Both are fully rounded, so the two capsules come out concentric by construction rather than by a number.

Anatomy

PartWhat it is
SwitchThe track. Owns the gesture, the spring and the state
Switch.ThumbThe knob. Composed in automatically; write it out to restyle it or fill it
Switch.StartContentBehind the thumb at the leading edge — revealed as the switch turns on
Switch.EndContentBehind the thumb at the trailing edge — revealed as the switch turns off
Start and end content
Start and end content
import { Icon } from "@delacour/native-ui/icon";import { IconCheckmark1Small, IconX } from "@delacour/native-ui/icons/central";import { SWITCH_SIZES, Switch } from "@delacour/native-ui/switch";import type { ReactElement } from "react";import { View } from "react-native";export function Demo(): ReactElement {	return (		<View className="flex-row items-center gap-4">			{SWITCH_SIZES.map((size) => (				<Switch color="primary" defaultSelected key={size} size={size} testID={`switch-${size}`}>					<Switch.StartContent>						<Icon icon={IconCheckmark1Small} />					</Switch.StartContent>					<Switch.EndContent>						<Icon icon={IconX} />					</Switch.EndContent>				</Switch>			))}		</View>	);}

Both layers are written once with no conditionals. StartContent is revealed as the switch turns on and EndContent as it turns off, each fading with the thumb's travel — so the knob reads as uncovering the other end. The glyphs take their size step and colour from the switch.

A hand-written Switch.Thumb is moved to the end of the children for you. Every part here is absolutely positioned and React Native paints later siblings on top, so a thumb written first — which is the order the anatomy reads best in — would slide under the content layers.

Drag as well as tap

The whole track is the target. A tap toggles; a drag carries the thumb and commits on release, with a fling past SWITCH_FLING_VELOCITY counting as a flip even when the thumb has not crossed the middle.

Movement is measured as Math.max(|translationX|, |translationY|). A vertical swipe that began on the switch has to count as movement, or every attempt to scroll past the control would read as a tap and toggle it.

The haptic fires at the commit, never at the grab.

The spring is critically damped

SWITCH_THUMB_SPRING has no overshoot, and that is deliberate: a wider thumb travels a shorter distance inside a track that clips it, so an overshoot has nowhere to go — the knob would visibly squash against the end of its own capsule on every toggle.

The colour the switch travels through and the press scale are two entries in one useAnimatedStyle, not two calls. Two animated styles on one view fight for the same props and the later one silently wins.

State inside a Field

In a settings list
In a settings list
import { Icon } from "@delacour/native-ui/icon";import { IconBell } from "@delacour/native-ui/icons/central";import { ListGroup } from "@delacour/native-ui/list-group";import { Switch } from "@delacour/native-ui/switch";import { Text } from "@delacour/native-ui/text";import { type ReactElement, useState } from "react";import { View } from "react-native";const SETTINGS = [	{ key: "wifi", title: "Wi-Fi", description: "Join known networks automatically" },	{ key: "bluetooth", title: "Bluetooth", description: "Discoverable while this screen is open" },	{ key: "airdrop", title: "AirDrop", description: "Receive from everyone for ten minutes" },] as const;export function Demo(): ReactElement {	const [settings, setSettings] = useState<Record<string, boolean>>({ wifi: true });	const enabled = Object.values(settings).filter(Boolean).length;	const toggleSetting = (key: string) => setSettings((current) => ({ ...current, [key]: !current[key] }));	return (		<View className="gap-3">			<ListGroup>				{SETTINGS.map((setting) => (					<ListGroup.Item key={setting.key} onPress={() => toggleSetting(setting.key)} testID={`row-${setting.key}`}>						<ListGroup.ItemPrefix>							<Icon icon={IconBell} />						</ListGroup.ItemPrefix>						<ListGroup.ItemContent>							<ListGroup.ItemTitle>{setting.title}</ListGroup.ItemTitle>							<ListGroup.ItemDescription>{setting.description}</ListGroup.ItemDescription>						</ListGroup.ItemContent>						<ListGroup.ItemSuffix>							<Switch								accessibilityLabel={setting.title}								color="success"								isSelected={settings[setting.key] ?? false}								onSelectedChange={() => toggleSetting(setting.key)}								size="sm"								testID={`switch-${setting.key}`}							/>						</ListGroup.ItemSuffix>					</ListGroup.Item>				))}			</ListGroup>			<Text.Caption>{`${enabled} of ${SETTINGS.length} settings on`}</Text.Caption>		</View>	);}

isDisabled and isInvalid are inherited from an enclosing Field, and the switch registers its toggle with the field — so the whole row drives it and a form switch can be a bare <Switch /> with Field.Label naming it. See Field.

Accessibility

Disabled and invalid
Disabled and invalid
import { Switch } from "@delacour/native-ui/switch";import type { ReactElement } from "react";import { View } from "react-native";export function Demo(): ReactElement {	return (		<View className="flex-row items-center gap-4">			<Switch color="primary" defaultSelected isDisabled />			<Switch color="primary" isDisabled />			<Switch defaultSelected isInvalid />			<Switch isInvalid />		</View>	);}

The root is not a Pressable, so there is no touch responder for TalkBack's activation to land on. The component supplies accessibilityActions and onAccessibilityTap itself, and marks the root accessible — without that the view is not an accessibility element on iOS and its role and state never reach VoiceOver.

There is no Switch.Label and no Switch.Group — use Field. And RTL is not handled: the thumb travels on translateX, which does not flip.

API

Prop

Type

ViewProps, minus children and style — the root writes its own animated style.

On this page