The starter comes with a set of basic components and a simple design system based on Nativewind to help you get started and save you time.
All those components can be found in the src/components/ui
folder. Our philosophy is to keep the components as simple as possible and to avoid adding too much logic to them. This way, they are easier to reuse and customize.
Based on your needs, you can either use them as they are or customize them to fit your needs. You can also create new ones based on the same approach.
Directory ui button.tsx checkbox.tsx colors.js focus-aware-status-bar.tsxDirectory icons/ image.tsx index.tsx input.tsx list.tsx modal.tsx progress-bar.tsx select.tsx text.tsx utils.tsx
List
The List
component references the FlashList component from the @shopify/flash-list
package.
import { FlashList as NFlashList } from ' @shopify/flash-list ' ;
import React from ' react ' ;
import { ActivityIndicator , View } from ' react-native ' ;
import Svg , { Circle , Path } from ' react-native-svg ' ;
import { Text } from ' ./text ' ;
export const List = NFlashList ;
export const EmptyList = React . memo (({ isLoading } : Props ) => {
< View className = " min-h-[400px] flex-1 items-center justify-center " >
< Text className = " pt-4 text-center " > Sorry! No data found </ Text >
export const NoData = () => (
< Svg width = { 200 } height = { 200 } viewBox = " 0 0 647.636 632.174 " >
d = " M411.146 142.174h-174.51a15.018 15.018 0 0 0-15 15v387.85l-2 .61-42.81 13.11a8.007 8.007 0 0 1-9.99-5.31l-127.34-415.95a8.003 8.003 0 0 1 5.31-9.99l65.97-20.2 191.25-58.54 65.97-20.2a7.99 7.99 0 0 1 9.99 5.3l32.55 106.32Z "
d = " m449.226 140.174-39.23-128.14a16.994 16.994 0 0 0-21.23-11.28l-92.75 28.39-191.24 58.55-92.75 28.4a17.015 17.015 0 0 0-11.28 21.23l134.08 437.93a17.027 17.027 0 0 0 16.26 12.03 16.79 16.79 0 0 0 4.97-.75l63.58-19.46 2-.62v-2.09l-2 .61-64.17 19.65a15.015 15.015 0 0 1-18.73-9.95L2.666 136.734a14.98 14.98 0 0 1 9.95-18.73l92.75-28.4 191.24-58.54 92.75-28.4a15.156 15.156 0 0 1 4.41-.66 15.015 15.015 0 0 1 14.32 10.61l39.05 127.56.62 2h2.08Z "
d = " M122.68 127.82a9.016 9.016 0 0 1-8.61-6.366l-12.88-42.072a8.999 8.999 0 0 1 5.97-11.24L283.1 14.278a9.009 9.009 0 0 1 11.24 5.971l12.88 42.072a9.01 9.01 0 0 1-5.97 11.241l-175.94 53.864a8.976 8.976 0 0 1-2.63.395Z "
< Circle cx = { 190.154 } cy = { 24.955 } r = { 20 } fill = " #7eb55a " />
< Circle cx = { 190.154 } cy = { 24.955 } r = { 12.665 } fill = " #fff " />
d = " M602.636 582.174h-338a8.51 8.51 0 0 1-8.5-8.5v-405a8.51 8.51 0 0 1 8.5-8.5h338a8.51 8.51 0 0 1 8.5 8.5v405a8.51 8.51 0 0 1-8.5 8.5Z "
d = " M447.136 140.174h-210.5a17.024 17.024 0 0 0-17 17v407.8l2-.61v-407.19a15.018 15.018 0 0 1 15-15h211.12Zm183.5 0h-394a17.024 17.024 0 0 0-17 17v458a17.024 17.024 0 0 0 17 17h394a17.024 17.024 0 0 0 17-17v-458a17.024 17.024 0 0 0-17-17Zm15 475a15.018 15.018 0 0 1-15 15h-394a15.018 15.018 0 0 1-15-15v-458a15.018 15.018 0 0 1 15-15h394a15.018 15.018 0 0 1 15 15Z "
d = " M525.636 184.174h-184a9.01 9.01 0 0 1-9-9v-44a9.01 9.01 0 0 1 9-9h184a9.01 9.01 0 0 1 9 9v44a9.01 9.01 0 0 1-9 9Z "
< Circle cx = { 433.636 } cy = { 105.174 } r = { 20 } fill = " #7eb55a " />
< Circle cx = { 433.636 } cy = { 105.174 } r = { 12.182 } fill = " #fff " />
Props
All @shopify/flash-list
Props are supported
We also provide an EmptyList
component that you can use to display a message when the list is empty. Feel free to customize it to fit your needs.
Use Case
import * as React from ' react ' ;
import { List , EmptyList , Text } from ' @/components/ui ' ;
const MyComponent = () => {
data = { [ ' Item 1 ' , ' Item 2 ' ] }
renderItem = { ({ item }) => < Text > { item } </ Text > }
ListEmptyComponent = { < EmptyList message = " No items " /> }
Image
For the Image
component, we use the expo-image
library to provide a fast and performant image component. The Image
component is a wrapper around the Image
component from expo-image
package with additional styling provided by nativewind
.
The cssInterop
function from nativewind
is used to apply styling and, in this way, the className
property is applied to the style
property of the Image
component.
import type { ImageProps } from ' expo-image ' ;
import { Image as NImage } from ' expo-image ' ;
import { cssInterop } from ' nativewind ' ;
import * as React from ' react ' ;
export type ImgProps = ImageProps & {
cssInterop ( NImage , { className : ' style ' });
placeholder = ' L6PZfSi_.AyE_3t7t7R**0o#DgR4 ' ,
placeholder = { placeholder }
export const preloadImages = ( sources : string []) => {
NImage . prefetch ( sources );
Props
All expo-image
Props are supported
className
- Tailwind CSS class names
Use Case
import * as React from ' react ' ;
import { Image } from ' @/components/ui ' ;
const MyComponent = () => {
uri : ' https://images.unsplash.com/photo-1524758631624-e2822e304c36 ' ,
Text
With this custom Text component, you can use the translation key as the tx
prop, and it will automatically translate the text based on the current locale, as well as support right-to-left (RTL) languages based on the selected locale.
import React from ' react ' ;
import type { TextProps , TextStyle } from ' react-native ' ;
import { I18nManager , StyleSheet , Text as NNText } from ' react-native ' ;
import { twMerge } from ' tailwind-merge ' ;
import type { TxKeyPath } from ' @/lib/i18n ' ;
import { translate } from ' @/lib/i18n ' ;
interface Props extends TextProps {
const textStyle = React . useMemo (
' text-base text-black dark:text-white font-inter font-normal ' ,
const nStyle = React . useMemo (
writingDirection : I18nManager . isRTL ? ' rtl ' : ' ltr ' ,
< NNText className = { textStyle } style = { nStyle } { ... props } >
{ tx ? translate ( tx ) : children }
Props
All React Native Text Props are supported
className
- Tailwind CSS class names
tx
- Translation key
Use Case
import * as React from ' react ' ;
import { Text , View } from ' react-native ' ;
const MyComponent = () => {
< View className = " flex flex-col items-center justify-center " >
< Text className = " text-2xl " tx = " welcome " />
< Text className = " text-md " className = " text-base " >
The starter comes with a simple Button
component that you can use to create a basic Pressable
with a Text
using Tailwind CSS classes and variant definitions. These variants’ logic is based on the tailwind-variants
package.
The tv
function from tailwind-variants
is used to create a function that generates a styling configuration object for the Button component based on slot definitions, variant, size , disabled status, full-width, and default variants. Consequently, the styles
defines the styles for the Button based on the provided props using the button
function.
Each variant should include styles for the container
, indicator
, and label
keys. The container
style is for the Pressable
, the label
style is for the Text
component, and the indicator
style is for the ActivityIndicator
component when the loading
prop is true
.
import React from ' react ' ;
import type { PressableProps , View } from ' react-native ' ;
import { ActivityIndicator , Pressable , Text } from ' react-native ' ;
import type { VariantProps } from ' tailwind-variants ' ;
import { tv } from ' tailwind-variants ' ;
container : ' my-2 flex flex-row items-center justify-center rounded-md px-4 ' ,
label : ' font-inter text-base font-semibold ' ,
indicator : ' h-6 text-white ' ,
container : ' bg-black dark:bg-white ' ,
label : ' text-white dark:text-black ' ,
indicator : ' text-white dark:text-black ' ,
container : ' bg-primary-600 ' ,
label : ' text-secondary-600 ' ,
container : ' border border-neutral-400 ' ,
label : ' text-black dark:text-neutral-100 ' ,
indicator : ' text-black dark:text-neutral-100 ' ,
container : ' bg-transparent ' ,
label : ' text-black underline dark:text-white ' ,
indicator : ' text-black dark:text-white ' ,
container : ' bg-transparent ' ,
icon : { container : ' size-9 ' },
container : ' bg-neutral-300 dark:bg-neutral-300 ' ,
label : ' text-neutral-600 dark:text-neutral-600 ' ,
indicator : ' text-neutral-400 dark:text-neutral-400 ' ,
container : ' self-center ' ,
type ButtonVariants = VariantProps < typeof button >;
interface Props extends ButtonVariants , Omit < PressableProps , ' disabled ' > {
export const Button = React . forwardRef < View , Props >(
const styles = React . useMemo (
() => button ({ variant , disabled , size }),
[ variant , disabled , size ],
disabled = { disabled || loading }
className = { styles . container ({ className }) }
className = { styles . indicator () }
testID = { testID ? ` ${ testID } -activity-indicator` : undefined }
testID = { testID ? ` ${ testID } -label` : undefined }
className = { styles . label ({ className : textClassName }) }
Props
All React Native Pressable Props are supported.
variant
- Button variant, one of variant
objects keys (default: default
)
loading
- Show loading indicator (default: false
)
label
- Button label
size
- Button size, one of variants size
objects keys (default: default
)
className
- Tailwind CSS class names to be applied to the Button’s container
textClassName
- Additional styling for the Button’s label
Use Case
import React from ' react ' ;
import { Button , View } from ' @/components/ui ' ;
import { Title } from ' ./title ' ;
export const Buttons = () => {
< View className = " flex-row flex-wrap " >
< Button label = " small " size = " sm " className = " mr-2 " />
className = " mr-2 min-w-[60px] "
< Button label = " small " size = " sm " variant = " outline " className = " mr-2 " />
< Button label = " small " size = " sm " variant = " ghost " className = " mr-2 " />
< Button label = " small " size = " sm " disabled className = " mr-2 " />
< Button label = " Default Button " />
< Button label = " Secondary Button " variant = " secondary " />
< Button label = " Outline Button " variant = " outline " />
< Button label = " Destructive Button " variant = " destructive " />
< Button label = " Ghost Button " variant = " ghost " />
< Button label = " Button " loading = { true } />
< Button label = " Button " loading = { true } variant = " outline " />
< Button label = " Default Button Disabled " disabled />
label = " Secondary Button Disabled "
We provide a simple Input
component with a Text
component for the label and a TextInput
component for the input.
You can use it in the same way you use the TextInput
component from React Native, but with additional props to customize the label and error styling.
The component utilizes the tv function from Tailwind Variants to define styling slots and variants for different states such as focused, error, and disabled. These styles are applied dynamically based on the component’s state and props.
We tried to keep the Input
component as simple as possible, but you can add more functionality, such as onFocus
and onBlur
, or adding left and right icons to the input.
import * as React from ' react ' ;
} from ' react-hook-form ' ;
import { useController } from ' react-hook-form ' ;
import type { TextInputProps } from ' react-native ' ;
import { I18nManager , StyleSheet , View } from ' react-native ' ;
import { TextInput as NTextInput } from ' react-native ' ;
import { tv } from ' tailwind-variants ' ;
import colors from ' ./colors ' ;
import { Text } from ' ./text ' ;
label : ' text-grey-100 mb-1 text-lg dark:text-neutral-100 ' ,
' mt-0 rounded-xl border-[0.5px] border-neutral-300 bg-neutral-100 px-4 py-3 font-inter text-base font-medium leading-5 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white ' ,
input : ' border-neutral-400 dark:border-neutral-300 ' ,
input : ' border-danger-600 ' ,
label : ' text-danger-600 dark:text-danger-600 ' ,
export interface NInputProps extends TextInputProps {
type TRule < T extends FieldValues > =
' disabled ' | ' valueAsNumber ' | ' valueAsDate ' | ' setValueAs '
export type RuleType < T extends FieldValues > = { [ name in keyof T ] : TRule < T > };
export type InputControllerType < T extends FieldValues > = {
interface ControlledInputProps < T extends FieldValues >
InputControllerType < T > {}
export const Input = React . forwardRef < NTextInput , NInputProps >(( props , ref ) => {
const { label , error , testID , ... inputProps } = props ;
const [ isFocussed , setIsFocussed ] = React . useState ( false );
const onBlur = React . useCallback (() => setIsFocussed ( false ), []);
const onFocus = React . useCallback (() => setIsFocussed ( true ), []);
const styles = React . useMemo (
disabled : Boolean ( props . disabled ),
[ error , isFocussed , props . disabled ],
< View className = { styles . container () } >
testID = { testID ? ` ${ testID } -label` : undefined }
className = { styles . label () }
placeholderTextColor = { colors . neutral [ 400 ] }
className = { styles . input () }
style = { StyleSheet . flatten ([
{ writingDirection : I18nManager . isRTL ? ' rtl ' : ' ltr ' },
{ textAlign : I18nManager . isRTL ? ' right ' : ' left ' },
testID = { testID ? ` ${ testID } -error` : undefined }
className = " text-sm text-danger-400 dark:text-danger-600 "
// only used with react-hook-form
export function ControlledInput < T extends FieldValues >(
props : ControlledInputProps < T >,
const { name , control , rules , ... inputProps } = props ;
const { field , fieldState } = useController ({ control , name , rules });
onChangeText = { field . onChange }
value = { ( field . value as string ) || '' }
error = { fieldState . error ?. message }
Props
All React Native TextInput Props are supported
label
- Input label
error
- Input error message
We provide also a simple ControlledInput
component that uses the Input
component under the hood but with a useController
hook from react-hook-form
to make it ready to use with react-hook-form
library.
Read more about Handling Forms here .
Use Case
import * as React from ' react ' ;
import { Input , View } from ' @/components/ui ' ;
const MyComponent = () => {
< View className = " flex flex-col items-center justify-center " >
< Input label = " Email " error = " Email is required " />
Modal
We provide a simple Modal
component using the @gorhom/bottom-sheet
library to display a modal at the bottom of the screen.
We opt to use a bottom sheet instead of a modal to make it more flexible and easy to use. as well as having full control over the logic and the UI.
Based on your needs, you can use the Modal
if you don’t have a fixed height for the modal content.
* - @gorhom/bottom-sheet.
* - All `BottomSheetModalProps` props.
* - `title` (string | undefined): Optional title for the modal header.
* import { Modal, useModal } from '@gorhom/bottom-sheet';
* function DisplayModal() {
* const { ref, present, dismiss } = useModal();
* snapPoints={['60%']} // optional
BottomSheetBackdropProps ,
} from ' @gorhom/bottom-sheet ' ;
import { BottomSheetModal , useBottomSheet } from ' @gorhom/bottom-sheet ' ;
import * as React from ' react ' ;
import { Pressable , View } from ' react-native ' ;
import Animated , { FadeIn , FadeOut } from ' react-native-reanimated ' ;
import { Path , Svg } from ' react-native-svg ' ;
import { Text } from ' ./text ' ;
type ModalProps = BottomSheetModalProps & {
type ModalRef = React . ForwardedRef < BottomSheetModal >;
type ModalHeaderProps = {
export const useModal = () => {
const ref = React . useRef < BottomSheetModal >( null );
const present = React . useCallback (( data ?: any ) => {
ref . current ?. present ( data );
const dismiss = React . useCallback (() => {
return { ref , present , dismiss };
export const Modal = React . forwardRef (
snapPoints : _snapPoints = [ ' 60% ' ],
const detachedProps = React . useMemo (
() => getDetachedProps ( detached ),
const modal = useModal ();
const snapPoints = React . useMemo (() => _snapPoints , [ _snapPoints ]);
React . useImperativeHandle (
() => ( modal . ref . current as BottomSheetModal ) || null ,
const renderHandleComponent = React . useCallback (
< View className = " mb-8 mt-2 h-1 w-12 self-center rounded-lg bg-gray-400 dark:bg-gray-700 " />
< ModalHeader title = { title } dismiss = { modal . dismiss } />
backdropComponent = { props . backdropComponent || renderBackdrop }
enableDynamicSizing = { false }
handleComponent = { renderHandleComponent }
const AnimatedPressable = Animated . createAnimatedComponent ( Pressable );
const CustomBackdrop = ({ style } : BottomSheetBackdropProps ) => {
const { close } = useBottomSheet ();
entering = { FadeIn . duration ( 50 ) }
exiting = { FadeOut . duration ( 20 ) }
style = { [ style , { backgroundColor : ' rgba(0, 0, 0, 0.4) ' }] }
export const renderBackdrop = ( props : BottomSheetBackdropProps ) => (
< CustomBackdrop { ... props } />
* In case the modal is detached, we need to add some extra props to the modal to make it look like a detached modal.
const getDetachedProps = ( detached : boolean ) => {
style : { marginHorizontal : 16 , overflow : ' hidden ' },
} as Partial < BottomSheetModalProps >;
return {} as Partial < BottomSheetModalProps >;
const ModalHeader = React . memo (({ title , dismiss } : ModalHeaderProps ) => {
< View className = " flex-row px-2 py-4 " >
< View className = " size-[24px] " />
< View className = " flex-1 " >
< Text className = " text-center text-[16px] font-bold text-[#26313D] dark:text-white " >
< CloseButton close = { dismiss } />
const CloseButton = ({ close } : { close : () => void }) => {
className = " absolute right-3 top-3 size-[24px] items-center justify-center "
hitSlop = { { top : 20 , bottom : 20 , left : 20 , right : 20 } }
accessibilityLabel = " close modal "
accessibilityRole = " button "
accessibilityHint = " closes the modal "
className = " fill-neutral-300 dark:fill-white "
< Path d = " M18.707 6.707a1 1 0 0 0-1.414-1.414L12 10.586 6.707 5.293a1 1 0 0 0-1.414 1.414L10.586 12l-5.293 5.293a1 1 0 1 0 1.414 1.414L12 13.414l5.293 5.293a1 1 0 0 0 1.414-1.414L13.414 12l5.293-5.293Z " />
Props
All @gorhom/bottom-sheet
Props are supported
children
- Modal content
title
: string
- Modal title
Use Case
import * as React from ' react ' ;
import { Modal , useModal , View , Button , Text } from ' @/components/ui ' ;
const MyComponent = () => {
const modal = useModal ();
< View className = " flex flex-col items-center justify-center " >
< Button variant = " primary " label = " Show Modal " onPress = { modal . present } />
< Modal ref = { modal . ref } title = " modal title " snapPoints = { [ ' 60% ' ] } >
< Text > Modal Content </ Text >
Select
We provide a simple Select
component using a bottom sheet with a simple List component to select an item from a list of items.
We opt to use a bottom sheet instead of a dropdown to make it more flexible and easy to use on both iOS and Android and also to minimize the number of dependencies in the starter.
The component uses the tv
function from Tailwind Variants to define styling slots and variants for different states such as error, and disabled. These styles are applied dynamically based on the component’s state and props.
Feel free to update the component implementation to fit your need and as you keep the same Props signature for the Select
component the component will work with our form handling solution without any changes.
/* eslint-disable max-lines-per-function */
} from ' @gorhom/bottom-sheet ' ;
import { FlashList } from ' @shopify/flash-list ' ;
import { useColorScheme } from ' nativewind ' ;
import * as React from ' react ' ;
import type { FieldValues } from ' react-hook-form ' ;
import { useController } from ' react-hook-form ' ;
import { Platform , View } from ' react-native ' ;
import { Pressable , type PressableProps } from ' react-native ' ;
import type { SvgProps } from ' react-native-svg ' ;
import Svg , { Path } from ' react-native-svg ' ;
import { tv } from ' tailwind-variants ' ;
import colors from ' @/components/ui/colors ' ;
import { CaretDown } from ' @/components/ui/icons ' ;
import type { InputControllerType } from ' ./input ' ;
import { useModal } from ' ./modal ' ;
import { Modal } from ' ./modal ' ;
import { Text } from ' ./text ' ;
label : ' text-grey-100 mb-1 text-lg dark:text-neutral-100 ' ,
' border-grey-50 mt-0 flex-row items-center justify-center rounded-xl border-[0.5px] p-3 dark:border-neutral-500 dark:bg-neutral-800 ' ,
inputValue : ' dark:text-neutral-100 ' ,
input : ' border-neutral-600 ' ,
input : ' border-danger-600 ' ,
label : ' text-danger-600 dark:text-danger-600 ' ,
inputValue : ' text-danger-600 ' ,
const List = Platform . OS === ' web ' ? FlashList : BottomSheetFlatList ;
export type OptionType = { label : string ; value : string | number };
onSelect : ( option : OptionType ) => void ;
function keyExtractor ( item : OptionType ) {
return `select-item- ${ item . value } ` ;
export const Options = React . forwardRef < BottomSheetModal , OptionsProps >(
({ options , onSelect , value , testID }, ref ) => {
const height = options .length * 70 + 100 ;
const snapPoints = React . useMemo (() => [ height ], [ height ]);
const { colorScheme } = useColorScheme ();
const isDark = colorScheme === ' dark ' ;
const renderSelectItem = React . useCallback (
({ item } : { item : OptionType }) => (
key = { `select-item- ${ item . value } ` }
selected = { value === item . value }
onPress = { () => onSelect ( item ) }
testID = { testID ? ` ${ testID } -item- ${ item . value } ` : undefined }
[ onSelect , value , testID ],
backgroundColor : isDark ? colors . neutral [ 800 ] : colors . white ,
keyExtractor = { keyExtractor }
renderItem = { renderSelectItem }
testID = { testID ? ` ${ testID } -modal` : undefined }
const Option = React . memo (
className = " flex-row items-center border-b border-neutral-300 bg-white px-3 py-2 dark:border-neutral-700 dark:bg-neutral-800 "
< Text className = " flex-1 dark:text-neutral-100 " > { label } </ Text >
export interface SelectProps {
onSelect ?: ( value : string | number ) => void ;
interface ControlledSelectProps < T extends FieldValues >
InputControllerType < T > {}
export const Select = ( props : SelectProps ) => {
placeholder = ' select... ' ,
const modal = useModal ();
const onSelectOption = React . useCallback (
( option : OptionType ) => {
onSelect ?.( option . value );
const styles = React . useMemo (
const textValue = React . useMemo (
? ( options ?. filter (( t ) => t . value === value )?.[ 0 ]?. label ?? placeholder )
[ value , options , placeholder ],
< View className = { styles . container () } >
testID = { testID ? ` ${ testID } -label` : undefined }
className = { styles . label () }
className = { styles . input () }
testID = { testID ? ` ${ testID } -trigger` : undefined }
< View className = " flex-1 " >
< Text className = { styles . inputValue () } > { textValue } </ Text >
testID = { ` ${ testID } -error` }
className = " text-sm text-danger-300 dark:text-danger-600 "
onSelect = { onSelectOption }
// only used with react-hook-form
export function ControlledSelect < T extends FieldValues >(
props : ControlledSelectProps < T >,
const { name , control , rules , onSelect : onNSelect , ... selectProps } = props ;
const { field , fieldState } = useController ({ control , name , rules });
const onSelect = React . useCallback (
( value : string | number ) => {
error = { fieldState . error ?. message }
const Check = ({ ... props } : SvgProps ) => (
className = " stroke-black dark:stroke-white "
d = " m20.256 6.75-10.5 10.5L4.506 12 "
Props
label
: string
- Input label
error
: string
- Input error message
options
: array of { label: string; value: string | number }
- List of items to select from
value
: string | number
- Selected item value
onSelect
: (option: Option) => void;
- Callback function to handle item selection
placeholder
: string
- Placeholder text
disabled
: boolean
- Disable select input (default: false
)
Use Case
import * as React from ' react ' ;
import type { Option } from ' @/components/ui ' ;
import { SelectInput , View } from ' @/components/ui ' ;
const options : Option [] = [
{ value : ' chocolate ' , label : ' Chocolate ' },
{ value : ' strawberry ' , label : ' Strawberry ' },
{ value : ' vanilla ' , label : ' Vanilla ' },
const MyComponent = () => {
const [ value , setValue ] = React . useState < string | number | undefined >();
< View className = " flex flex-col items-center justify-center " >
error = " Select is required "
onSelect = { ( option ) => setValue ( option . value ) }
Controlled Select
We provide a simple ControlledSelect
component that uses the Select
component under the hood but with a useController
hook from react-hook-form
to make it ready to use with react-hook-form
library.
Read more about Handling Forms here .
Checkbox, Radio & Switch
We provide a set of three simple and customizable components including a Checkbox
, a Radio
, and a Switch
, which share the same logic under the hood.
The Checkbox
, Switch
, and Radio
components are very similar as they share a common structure and are supposed to handle boolean values, their primary difference being the icon they display and the associated accessibility label. Each component accepts a range of props, allowing us to customize their appearance, behavior, and accessibility features.
For handling common functionality like handling press events and accessibility states we have the Root
component. It wraps its children in a Pressable
component and passes along props.
Animations are applied to the icons using the MotiView
component from the moti
library. These animations change the appearance of the icons based on their checked state.
import { MotiView } from ' moti ' ;
import React , { useCallback } from ' react ' ;
import Svg , { Path } from ' react-native-svg ' ;
import colors from ' @/components/ui/colors ' ;
import { Text } from ' ./text ' ;
export interface RootProps extends Omit < PressableProps , ' onPress ' > {
onChange : ( checked : boolean ) => void ;
accessibilityLabel : string ;
export type IconProps = {
const handleChange = useCallback (() => {
className = { `flex-row items-center ${ className } ${
disabled ? ' opacity-50 ' : ''
accessibilityState = { { checked } }
const Label = ({ text , testID , className = '' } : LabelProps ) => {
< Text testID = { testID } className = { ` ${ className } pl-2` } >
export const CheckboxIcon = ({ checked = false } : IconProps ) => {
const color = checked ? colors . primary [ 300 ] : colors . charcoal [ 400 ];
className = " items-center justify-center rounded-[5px] border-2 "
from = { { backgroundColor : ' transparent ' , borderColor : ' #CCCFD6 ' } }
backgroundColor : checked ? color : ' transparent ' ,
backgroundColor : { type : ' timing ' , duration : 100 },
borderColor : { type : ' timing ' , duration : 100 },
animate = { { opacity : checked ? 1 : 0 } }
transition = { { opacity : { type : ' timing ' , duration : 100 } } }
< Svg width = " 24 " height = " 24 " viewBox = " 0 0 24 24 " fill = " none " >
d = " m16.726 7-.64.633c-2.207 2.212-3.878 4.047-5.955 6.158l-2.28-1.928-.69-.584L6 12.66l.683.577 2.928 2.477.633.535.591-.584c2.421-2.426 4.148-4.367 6.532-6.756l.633-.64L16.726 7Z "
const CheckboxRoot = ({ checked = false , children , ... props } : RootProps ) => {
< Root checked = { checked } accessibilityRole = " checkbox " { ... props } >
} : RootProps & { label ?: string }) => {
< CheckboxRoot checked = { checked } testID = { testID } { ... props } >
< CheckboxIcon checked = { checked } />
testID = { testID ? ` ${ testID } -label` : undefined }
export const Checkbox = Object . assign ( CheckboxBase , {
export const RadioIcon = ({ checked = false } : IconProps ) => {
const color = checked ? colors . primary [ 300 ] : colors . charcoal [ 400 ];
className = " items-center justify-center rounded-[20px] border-2 bg-transparent "
from = { { borderColor : ' #CCCFD6 ' } }
transition = { { borderColor : { duration : 100 , type : ' timing ' } } }
className = { `size-[10px] rounded-[10px] ${ checked && ' bg-primary-300 ' } ` }
animate = { { opacity : checked ? 1 : 0 } }
transition = { { opacity : { duration : 50 , type : ' timing ' } } }
const RadioRoot = ({ checked = false , children , ... props } : RootProps ) => {
< Root checked = { checked } accessibilityRole = " radio " { ... props } >
} : RootProps & { label ?: string }) => {
< RadioRoot checked = { checked } testID = { testID } { ... props } >
< RadioIcon checked = { checked } />
< Label text = { label } testID = { testID ? ` ${ testID } -label` : undefined } />
export const Radio = Object . assign ( RadioBase , {
export const SwitchIcon = ({ checked = false } : IconProps ) => {
const translateX = checked
: WIDTH - THUMB_WIDTH - THUMB_OFFSET ;
const backgroundColor = checked ? colors . primary [ 300 ] : colors . charcoal [ 400 ];
< View className = " w-[50px] justify-center " >
< View className = " overflow-hidden rounded-full " >
backgroundColor : ' white ' ,
translateX : I18nManager . isRTL ? translateX : - translateX ,
transition = { { translateX : { overshootClamping : true } } }
const SwitchRoot = ({ checked = false , children , ... props } : RootProps ) => {
< Root checked = { checked } accessibilityRole = " switch " { ... props } >
} : RootProps & { label ?: string }) => {
< SwitchRoot checked = { checked } testID = { testID } { ... props } >
< SwitchIcon checked = { checked } />
< Label text = { label } testID = { testID ? ` ${ testID } -label` : undefined } />
export const Switch = Object . assign ( SwitchBase , {
Props
All React Native Pressable Props are supported excluding onPress
prop
onChange
- (checked: boolean) => void;` - Callback function to handle component’s state
checked
- boolean
- Determines the state of the component (default:false
)
label
- Component’s label
accessibilityLabel
- Component’s accessibility label
children
- Child components/elements
className
- Tailwind CSS class names
disabled
: boolean
- Disable component (default: false
)
Use Case
import { Checkbox } from ' @/components/ui ' ;
const [ checked , setChecked ] = useState ( false );
accessibilityLabel = " accept terms of condition "
label = " I accept terms and conditions "
By default the component will render a label with the text you passed as label prop and clicking on the label will toggle the component as well.
For rendering a custom Checkbox, you can use the Checkbox.Root
, Checkbox.Icon
, and Checkbox.Label
components.
import { Checkbox } from ' @/components/ui ' ;
const [ checked , setChecked ] = useState ( false );
accessibilityLabel = " accept terms of condition "
< Checkbox.Icon checked = { checked } />
< Checkbox.Label text = " I agree to terms and conditions " />