Button
A TouchableOpacity-based button with variants, slots, and automatic variant context propagation. Copy and adapt to your design system.
import { TouchableOpacity, Text } from 'react-native';import { createComponent } from 'react-native-small-ui';
const Button = createComponent(TouchableOpacity, { base: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', borderRadius: 8, }, variants: { intent: { primary: { _light: { backgroundColor: '#007AFF' }, _dark: { backgroundColor: '#0A84FF' }, }, danger: { _light: { backgroundColor: '#e00c2c' }, _dark: { backgroundColor: '#be0a25' }, }, ghost: { backgroundColor: 'transparent', _light: { borderWidth: 1, borderColor: '#007AFF' }, _dark: { borderWidth: 1, borderColor: '#0A84FF' }, }, }, size: { sm: { paddingVertical: 6, paddingHorizontal: 12 }, md: { paddingVertical: 10, paddingHorizontal: 20 }, lg: { paddingVertical: 14, paddingHorizontal: 28 }, }, }, defaultVariants: { intent: 'primary', size: 'md', },}).withSlots({ // Button.Text and Button.Icon define the same intent variant as Button. // withVariantContext below propagates the parent intent automatically — // no prop needed on the slot at the call site. Text: createComponent(Text, { fontSize: 15, fontWeight: '600', variants: { intent: { primary: { _light: { color: '#fff' }, _dark: { color: '#fff' } }, danger: { _light: { color: '#fff' }, _dark: { color: '#fff' } }, ghost: { _light: { color: '#007AFF' }, _dark: { color: '#0A84FF' } }, }, }, defaultVariants: { intent: 'primary' }, }), Icon: createComponent(Text, { fontSize: 16, marginRight: 6, variants: { intent: { primary: { _light: { color: '#fff' }, _dark: { color: '#fff' } }, danger: { _light: { color: '#fff' }, _dark: { color: '#fff' } }, ghost: { _light: { color: '#007AFF' }, _dark: { color: '#0A84FF' } }, }, }, defaultVariants: { intent: 'primary' }, }),}).withVariantContext('intent');// intent on Button flows to Button.Text and Button.Icon automatically.// Explicit props on a slot always override the context.intent propagates from Button to its slots — no prop needed on Button.Text or Button.Icon.
// primary is the default — no props needed at all<Button onPress={onSubmit}> <Button.Text>Submit</Button.Text></Button>
// with icon — both slots pick up the active intent<Button intent="primary" size="lg" onPress={onConfirm}> <Button.Icon>✓</Button.Icon> <Button.Text>Confirm</Button.Text></Button>
<Button intent="danger" size="sm" onPress={onDelete}> <Button.Text>Delete</Button.Text></Button>
<Button intent="ghost" onPress={onCancel}> <Button.Text>Cancel</Button.Text></Button>Explicit slot props always override the context — useful when you need a slot styled differently from its parent:
<Button intent="ghost"> <Button.Icon intent="primary">→</Button.Icon> <Button.Text>Continue</Button.Text></Button>Platform-specific shadow
Section titled “Platform-specific shadow”Extend the base definition to layer in platform styles:
const Button = createComponent(TouchableOpacity, { // ... variants and slots as above base: { alignItems: 'center', justifyContent: 'center', borderRadius: 8, _ios: { shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 4, shadowOffset: { width: 0, height: 2 } }, _android: { elevation: 4 }, }, // ...});How it works
Section titled “How it works”Button uses three APIs in combination:
| API | Role |
|---|---|
variants + defaultVariants |
Type-safe intent and size props with autocomplete |
.withSlots() |
Attaches Button.Text and Button.Icon as dot-notation sub-components |
.withVariantContext('intent') |
Propagates the parent’s active intent to slots via React context — no prop drilling |
Slots share colorMode, breakpoints, and the specified variant keys with the parent. Two Button trees never share context — each compound instance is isolated.