Switch

A toggle between two states

Switch exposes a controlled or uncontrolled boolean state through accessible, independently styled frame and thumb parts.

Features

  • Accessible, composable, and customizable

  • Independent frame and thumb styling and animation

  • Sizing with controlled or uncontrolled state

  • Native mobile Switch through the native prop

Installation

Switch is already installed in tamagui, or you can install it independently:

yarn add @tamagui/switch

Usage

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Switch.tsx (registry item "switch")
// npm dependencies: @tamagui/core @tamagui/switch
import { Switch } from "../components/tamagui/Switch"; // or '@tamagui/switch'
export default () => <Switch size="md">
<Switch.Thumb transition="bouncy" />
</Switch>;

Headless usage

To use the headless switch without @tamagui/core, install @tamagui/switch-headless:

yarn add @tamagui/switch-headless

The useSwitch hook provides all the state and accessibility props needed to build a custom switch with any styling solution:

import { useSwitch } from '@tamagui/switch-headless'
import { useState } from 'react'
import { Pressable, View } from 'react-native'
function MySwitch({ defaultChecked, onCheckedChange, ...props }) {
const [checked, setChecked] = useState(defaultChecked || false)
const { switchProps, switchRef, bubbleInput } = useSwitch(
props,
[checked, setChecked],
null
)
return (
<>
<Pressable ref={switchRef} {...switchProps} style={{ width: 50, height: 28, borderRadius: 14, backgroundColor: checked ? '#22c55e' : '#d1d5db', padding: 2, }} >
<View style={{ width: 24, height: 24, borderRadius: 12, backgroundColor: 'white', transform: [{ translateX: checked ? 22 : 0 }], }} />
</Pressable>
{bubbleInput}
</>
)
}

API reference

Switch

Switch extends View, inheriting all the Tamagui standard props. Sizes come from the shared Sizing ladder.

Props

  • size

    'xs' | 'sm' | 'md' | 'lg' | 'xl'

    A named size ("xs" to "xl", default "md") is a rung in the sizing ladder: the track is the ladder's square stretched 1.9 wide, and the thumb insets 2px on every side.

  • labeledBy

    string

    Set aria-labeled-by.

  • name

    string

    Equivalent to input name.

  • value

    string

    Give it a value (for use in HTML forms).

  • checked

    boolean

    Control the input.

  • defaultChecked

    boolean

    Uncontrolled default value.

  • required

    boolean

    Sets aria-required.

  • onCheckedChange

    (checked: boolean) => void

    Callback called when checked state changes.

  • unstyled

    boolean

    Default: 

    false

    When true, removes all default Tamagui styling.

  • native

    NativeValue<"mobile" | "ios" | "android">

    Render to a native switch. (Not supported on web)

  • nativeProps

    SwitchProps (from `react-native`)

    Props to pass to the native Switch.

  • activeStyle

    ViewStyle

    Styles to apply when the switch is checked/active.

  • activeTheme

    string | null

    Theme to apply when the switch is checked/active.

  • Switch.Thumb

    Switch.Thumb extends View inheriting all the Tamagui standard props, plus:

    Props

  • unstyled

    boolean

    Default: 

    false

    When true, removes all default Tamagui styling.

  • activeStyle

    ViewStyle

    Styles to apply to the thumb when the switch is checked/active.

  • useSwitch

    The useSwitch hook accepts three arguments:

    const { switchProps, switchRef, bubbleInput } = useSwitch(
    props, // SwitchProps
    state, // [checked: boolean, setChecked: (checked: boolean) => void]
    ref // React.Ref
    )

    Props (first argument)

    Props

  • labeledBy

    string

    Set aria-labelledby for accessibility.

  • disabled

    boolean

    Whether the switch is disabled.

  • name

    string

    Form input name for the hidden input.

  • value

    string

    Form input value.

  • required

    boolean

    Whether the switch is required in a form.

  • onPress

    (event) => void

    Called when switch is pressed (composed with internal handler).

  • State (second argument)

    A tuple of [checked, setChecked] where:

    • checked: Current boolean state
    • setChecked: React state setter function

    Return value

    PropertyTypeDescription
    switchPropsobjectProps to spread on your switch element (role, aria-checked, onPress, etc.)
    switchRefRefComposed ref to attach to your switch element
    bubbleInputReactNode | nullHidden input for form compatibility (render as sibling, web only)
    Property
    switchProps
    Type
    object
    Description
    Props to spread on your switch element (role, aria-checked, onPress, etc.)
    Property
    switchRef
    Type
    Ref
    Description
    Composed ref to attach to your switch element
    Property
    bubbleInput
    Type
    ReactNode | null
    Description
    Hidden input for form compatibility (render as sibling, web only)

    Source

    v2-look Switch: token-based frame and thumb sizing with theme background and focus styling, over the unstyled @tamagui/ui Switch behavior. This is the exact source the registry ships — copy it into your app and the examples above import from your copy.

    components/tamagui/Switch.tsx
    import { createSwitch, SwitchFrame as SwitchBehaviorFrame, SwitchThumbFrame as SwitchBehaviorThumbFrame, } from '@tamagui/switch' import { type ComponentSize, type GetProps, resolveSizing, styled } from '@tamagui/core' export type SwitchSize = ComponentSize | boolean const THUMB_INSET = 2 // the track is the ladder's square stretched into a pill (1.9 wide), so a // switch and a checkbox at the same size read as the same weight const getSwitchTrackSize = styled.dynamic<SwitchSize>((val, env) => { const sizing = resolveSizing(val, env) if (!sizing) return return { width: Math.round(sizing.square * 1.9), height: sizing.square, minHeight: sizing.square, } }) // the thumb insets by the track's 2px padding on every side const getSwitchThumbSize = styled.dynamic<SwitchSize>((val, env) => { const sizing = resolveSizing(val, env) if (!sizing) return const side = sizing.square - THUMB_INSET * 2 return { width: side, height: side, } }) export const SwitchFrame = styled(SwitchBehaviorFrame, { displayName: 'Switch', borderRadius: 1000, padding: THUMB_INSET, outlineColor: 'focus-visible:outline-color', outlineStyle: 'focus-visible:solid', outlineWidth: 'focus-visible:2px', variants: { size: getSwitchTrackSize, // off is a low-contrast trough the page barely registers. on swaps the whole // control onto the brand theme, so `background` here is the inverse fill and // the thumb's `color` is the matching foreground. active: { true: { backgroundColor: 'background' }, false: { backgroundColor: 'color-4 hover:color-5' }, }, disabled: { true: { cursor: 'not-allowed', opacity: 0.45 }, }, } as const, defaultVariants: { size: 'md', }, }) export const SwitchThumbFrame = styled(SwitchBehaviorThumbFrame, { displayName: 'SwitchThumb', borderRadius: 1000, boxShadow: '0 1px 2px rgba(0,0,0,0.18)', variants: { size: getSwitchThumbSize, // the knob stays the light one against the trough in both schemes, and // becomes the brand foreground once the track flips to the brand fill. active: { true: { backgroundColor: 'color' }, false: { backgroundColor: 'white' }, }, } as const, defaultVariants: { size: 'md', }, }) export const Switch = createSwitch({ Frame: SwitchFrame, Thumb: SwitchThumbFrame, activeTheme: 'brand', }) export const SwitchThumb = Switch.Thumb export type SwitchProps = GetProps<typeof Switch>

    Dependencies

    yarn add @tamagui/core @tamagui/switch

    Expects theme tokens: background, outline-color.

    Need raw behavior without any skin? tamagui/unstyled re-exports the @tamagui/ui primitives (advanced).