Installation
npx @park-ui/cli@next add switchAdd Component
Copy the code snippet below into you components folder.
'use client'
import { ark } from '@ark-ui/react'
import { Switch, useSwitchContext } from '@ark-ui/react/switch'
import { type ComponentProps, forwardRef, type ReactNode } from 'react'
import { createStyleContext, styled } from 'styled-system/jsx'
import { switchRecipe } from 'styled-system/recipes'
const { withProvider, withContext } = createStyleContext(switchRecipe)
export type RootProps = ComponentProps<typeof Root>
export const Root = withProvider(Switch.Root, 'root')
export const RootProvider = withProvider(Switch.RootProvider, 'root')
export const Label = withContext(Switch.Label, 'label')
export const Thumb = withContext(Switch.Thumb, 'thumb')
export const HiddenInput = Switch.HiddenInput
export const Control = withContext(Switch.Control, 'control', {
defaultProps: { children: <Thumb /> },
})
export { SwitchContext as Context } from '@ark-ui/react/switch'
interface IndicatorProps extends ComponentProps<typeof StyledIndicator> {
fallback?: ReactNode | undefined
}
const StyledIndicator = withContext(ark.span, 'indicator')
export const Indicator = forwardRef<HTMLSpanElement, IndicatorProps>(
function Indicator(props, ref) {
const { fallback, children, ...rest } = props
const api = useSwitchContext()
return (
<StyledIndicator ref={ref} data-checked={api.checked ? '' : undefined} {...rest}>
{api.checked ? children : fallback}
</StyledIndicator>
)
},
)
interface ThumbIndicatorProps extends ComponentProps<typeof StyledThumbIndicator> {
fallback?: React.ReactNode | undefined
}
const StyledThumbIndicator = styled(ark.span)
export const ThumbIndicator = forwardRef<HTMLSpanElement, ThumbIndicatorProps>(
function SwitchThumbIndicator(props, ref) {
const { fallback, children, ...rest } = props
const api = useSwitchContext()
return (
<StyledThumbIndicator ref={ref} data-checked={api.checked ? '' : undefined} {...rest}>
{api.checked ? children : fallback}
</StyledThumbIndicator>
)
},
)
Integrate Recipe
Integrate this recipe in to your Panda config.
import { switchAnatomy } from '@ark-ui/react/switch'
import { defineSlotRecipe } from '@pandacss/dev'
export const switchRecipe = defineSlotRecipe({
className: 'switch',
jsx: ['Switch', /Switch\.+/],
slots: switchAnatomy.extendWith('indicator').keys(),
base: {
root: {
display: 'inline-flex',
alignItems: 'center',
position: 'relative',
verticalAlign: 'middle',
'--switch-diff': 'calc(var(--switch-width) - var(--switch-height))',
'--switch-x': {
base: 'var(--switch-diff)',
_rtl: 'calc(var(--switch-diff) * -1)',
},
},
label: {
fontWeight: 'medium',
userSelect: 'none',
lineHeight: '1',
},
indicator: {
position: 'absolute',
height: 'var(--switch-height)',
width: 'var(--switch-height)',
fontSize: 'var(--switch-indicator-font-size)',
fontWeight: 'medium',
flexShrink: 0,
userSelect: 'none',
display: 'grid',
placeContent: 'center',
transition: 'inset-inline-start 0.12s ease',
insetInlineStart: 'calc(var(--switch-x) - 2px)',
_checked: {
insetInlineStart: '2px',
},
},
control: {
display: 'inline-flex',
gap: '0.5rem',
flexShrink: 0,
justifyContent: 'flex-start',
cursor: 'pointer',
borderRadius: 'full',
position: 'relative',
width: 'var(--switch-width)',
height: 'var(--switch-height)',
transition: 'backgrounds',
focusVisibleRing: 'outside',
_disabled: {
layerStyle: 'disabled',
},
_invalid: {
outline: '2px solid',
outlineColor: 'error',
outlineOffset: '2px',
},
},
thumb: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
transitionProperty: 'translate',
transitionDuration: 'fast',
borderRadius: 'inherit',
_checked: {
translate: 'var(--switch-x) 0',
},
},
},
defaultVariants: {
variant: 'solid',
size: 'md',
},
variants: {
variant: {
solid: {
control: {
borderRadius: 'full',
bg: 'gray.subtle.bg',
focusVisibleRing: 'outside',
_checked: {
bg: 'colorPalette.solid.bg',
},
},
thumb: {
bg: 'white',
_checked: {
bg: 'colorPalette.solid.fg',
},
width: 'var(--switch-height)',
height: 'var(--switch-height)',
scale: '0.8',
boxShadow: 'xs',
},
},
},
size: {
xs: {
root: {
gap: '2',
'--switch-width': 'sizes.8',
'--switch-height': 'sizes.4',
'--switch-indicator-font-size': 'fontSizes.xs',
},
label: { fontSize: 'sm' },
},
sm: {
root: {
gap: '2',
'--switch-width': 'sizes.9',
'--switch-height': 'sizes.4.5',
'--switch-indicator-font-size': 'fontSizes.xs',
},
label: { fontSize: 'sm' },
},
md: {
root: {
gap: '3',
'--switch-width': 'sizes.10',
'--switch-height': 'sizes.5',
'--switch-indicator-font-size': 'fontSizes.sm',
},
label: { fontSize: 'md' },
},
lg: {
root: {
gap: '3',
'--switch-width': 'sizes.11',
'--switch-height': 'sizes.5.5',
'--switch-indicator-font-size': 'fontSizes.md',
},
label: { fontSize: 'lg' },
},
},
},
})
Usage
import { Switch } from '@/components/ui'
<Switch.Root>
<Switch.HiddenInput />
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
<Switch.Label />
</Switch.Root>
Shortcuts
The Switch component also provides a set of shortcuts for common use cases.
SwitchControl
The Switch.Control renders the Switch.Thumb within it by default.
This works:
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
This might be more concise, if you don't need to customize the thumb:
<Switch.Control />
Examples
Sizes
Use the size prop to change the size of the switch.
Controlled
Use the checked and onCheckedChange prop to control the state of the switch.
Disabled
Use the disabled prop to disable the switch.
Invalid
Use the invalid prop to mark the switch as invalid.
Tooltip
Wrap the Switch.Root with a Tooltip to show a tooltip on hover or focus.
Track Indicator
Use the Switch.Indicator component to display different indicators based on the checked state.
Thumb Indicator
Use the Switch.ThumbIndicator component to add an icon to the switch thumb.
Closed Component
Here's how to setup the switch for a closed component composition.
import { forwardRef, type InputHTMLAttributes, type ReactNode, type RefObject } from 'react'
import { Switch as ParkSwitch } from '@/components/ui'
export interface SwitchProps extends ParkSwitch.RootProps {
inputProps?: InputHTMLAttributes<HTMLInputElement>
rootRef?: RefObject<HTMLLabelElement | null>
trackLabel?: { on: ReactNode; off: ReactNode }
thumbLabel?: { on: ReactNode; off: ReactNode }
}
export const Switch = forwardRef<HTMLInputElement, SwitchProps>(function Switch(props, ref) {
const { inputProps, children, rootRef = null, trackLabel, thumbLabel, ...rest } = props
return (
<ParkSwitch.Root ref={rootRef} {...rest}>
<ParkSwitch.HiddenInput ref={ref} {...inputProps} />
<ParkSwitch.Control>
<ParkSwitch.Thumb>
{thumbLabel && (
<ParkSwitch.ThumbIndicator fallback={thumbLabel?.off}>
{thumbLabel?.on}
</ParkSwitch.ThumbIndicator>
)}
</ParkSwitch.Thumb>
{trackLabel && (
<ParkSwitch.Indicator fallback={trackLabel.off}>{trackLabel.on}</ParkSwitch.Indicator>
)}
</ParkSwitch.Control>
{children != null && <ParkSwitch.Label>{children}</ParkSwitch.Label>}
</ParkSwitch.Root>
)
})
Props
Root
| Prop | Default | Type |
|---|---|---|
variant | 'solid' | 'solid' |
size | 'md' | 'xs' | 'sm' | 'md' | 'lg' |
value | \on\ | string | numberThe value of switch input. Useful for form submission. |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. | |
checked | booleanThe controlled checked state of the switch | |
disabled | booleanWhether the switch is disabled. | |
ids | Partial<{ root: string; hiddenInput: string; control: string; label: string; thumb: string }>The ids of the elements in the switch. Useful for composition. | |
invalid | booleanIf `true`, the switch is marked as invalid. | |
label | stringSpecifies the localized strings that identifies the accessibility elements and their states | |
name | stringThe name of the input field in a switch (Useful for form submission). | |
onCheckedChange | (details: CheckedChangeDetails) => voidFunction to call when the switch is clicked. | |
readOnly | booleanWhether the switch is read-only | |
required | booleanIf `true`, the switch input is marked as required, |