Patterns
Common patterns and conventions used throughout Hyena components.
Compound Components
Many Hyena components use a compound component pattern — a root component with multiple sub-components that work together.
The Pattern
import {
DatePicker,
DatePickerTrigger,
DatePickerContent,
DatePickerCalendar,
Button,
} from '@hyena-studio/react-native'
<DatePicker>
<DatePickerTrigger>
<Button>Pick a date</Button>
</DatePickerTrigger>
<DatePickerContent>
<DatePickerCalendar />
</DatePickerContent>
</DatePicker>
Why Compound Components?
- Flexibility — Rearrange, hide, or customize any part
- Composition — Add your own components between parts
- Context — State is shared automatically between parts
- Accessibility — ARIA relationships are handled for you
Hyena Compound Components
Here are the compound component structures in Hyena:
DatePicker:
DatePicker → DatePickerTrigger → DatePickerContent → DatePickerHeader → DatePickerCalendar
TimePicker:
TimePicker → TimePickerTrigger → TimePickerContent → TimePickerList
Dialog:
Dialog → DialogTrigger → DialogContent → DialogHeader → DialogTitle → DialogDescription → DialogFooter → DialogClose
Sheet:
Sheet → SheetTrigger → SheetContent → SheetHeader → SheetFooter
Accordion:
Accordion → AccordionItem → AccordionTrigger → AccordionContent
Tabs:
Tabs → TabsList → TabsTrigger → TabsContent
Common Sub-Component Names
| Name | Purpose |
|---|---|
Trigger | Element that opens/activates the component |
Content | The main content area (popover, modal, etc.) |
Header | Header section of content |
Footer | Footer section of content |
Title | Title text with proper heading semantics |
Description | Description text |
Close | Close button/trigger |
Item | Individual item in a list/menu |
Overlay | Background overlay/backdrop |
Context Hooks
Every compound component exposes a context hook for advanced use cases:
import { useDatePicker, Pressable, Text } from '@hyena-studio/react-native'
function CustomDateTrigger() {
const { open, setOpen, value } = useDatePicker()
return (
<Pressable onPress={() => setOpen(true)}>
<Text>{value ? format(value, 'PP') : 'Select date'}</Text>
</Pressable>
)
}
Available Context Hooks
| Hook | Returns |
|---|---|
useDatePicker() | { open, setOpen, value, onValueChange, mode, minDate, maxDate } |
useTimePicker() | { open, setOpen, value, onValueChange, use24Hour, interval, minTime, maxTime } |
useDialog() | { open, setOpen } |
useSheet() | { open, setOpen, side } |
useAccordion() | { value, onValueChange, type } |
useTabs() | { value, onValueChange } |
useDropdown() | { open, onOpenChange, triggerLayout } |
Props Patterns
Variants and Sizes
Most visible components support variant and size props:
// Button variants
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
// Size variants (consistent across components)
<Button size="sm">Small</Button>
<Button size="md">Medium (default)</Button>
<Button size="lg">Large</Button>
Button Variants:
| Variant | Use Case |
|---|---|
primary | Main action, prominent blue styling |
secondary | Secondary action, subtle styling |
ghost | Minimal styling, toolbars, subtle actions |
destructive | Dangerous actions, red styling |
Size Variants:
| Size | Height | Use Case |
|---|---|---|
sm | 32px | Compact UI, toolbars |
md | 40px | Default, forms |
lg | 48px | Prominent CTAs |
Controlled vs Uncontrolled
Components can be controlled (you manage state) or uncontrolled (internal state):
// Uncontrolled — component manages its own state
<Switch defaultChecked={true} />
// Controlled — you manage state
const [checked, setChecked] = useState(true)
<Switch checked={checked} onCheckedChange={setChecked} />
Uncontrolled
Simple forms, quick prototypes. Component manages its own state.
Controlled
Complex forms, validation, derived state. You manage state externally.
Value and onChange Patterns
Form components follow consistent naming:
| Component | Value Prop | Change Handler |
|---|---|---|
| Input | value | onChangeText |
| Textarea | value | onChangeText |
| Checkbox | checked | onCheckedChange |
| Switch | checked | onCheckedChange |
| Select | value | onValueChange |
| Slider | value | onValueChange |
| DatePicker | value | onValueChange |
| TimePicker | value | onValueChange |
| RadioGroup | value | onValueChange |
| Tabs | value | onValueChange |
| Accordion | value | onValueChange |
Disabled State
All interactive components support disabled:
<Button disabled>Can't click</Button>
<Input disabled value="Read only" />
<Switch disabled checked />
Disabled components:
- Reduce opacity
- Remove hover/press states
- Prevent interaction
- Set
aria-disabledfor accessibility
Loading State
Components that trigger async actions support loading:
<Button loading>Saving...</Button>
<Button loading loadingText="Please wait">Submit</Button>
Slot Pattern
Some components accept content via props instead of children:
Icons with Components
<Button icon={<IconPlus />}>Add Item</Button>
<Input leftIcon={<IconSearch />} placeholder="Search..." />
<Alert icon={<IconWarning />} title="Warning" />
When to Use Props vs Children
Use children when:
- Content is the primary purpose (Button label, Card content)
- Content structure varies widely
Use props when:
- Content is supplementary (icons, badges)
- Content has a specific position (left/right icons)
- Content is optional decoration
Styling Patterns
Style Props
Most components accept standard React Native style props:
<Button style={{ marginTop: 16 }}>Styled</Button>
<Card style={{ backgroundColor: '#f0f0f0' }}>Custom Card</Card>
Specific Style Props
Some components expose granular style props:
<Button
style={{ marginHorizontal: 8 }} // Container
textStyle={{ fontWeight: 'bold' }} // Text inside
/>
<Input
style={{ marginBottom: 12 }} // Container
inputStyle={{ textAlign: 'right' }} // TextInput
/>
NativeWind / className
On web and with NativeWind configured, you can use className:
<Button className="mt-4 hover:scale-105">
Tailwind Styled
</Button>
Composition Patterns
Wrapping Components
Create your own variants by wrapping Hyena components:
function PrimaryButton({ children, ...props }) {
return (
<Button
variant="primary"
size="lg"
style={{ minWidth: 200 }}
{...props}
>
{children}
</Button>
)
}
Compound Component Composition
Extend compound components with your own logic:
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose,
Button,
} from '@hyena-studio/react-native'
function ConfirmDialog({ title, description, onConfirm, destructive, children }) {
return (
<Dialog>
<DialogTrigger asChild>
{children}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="ghost">Cancel</Button>
</DialogClose>
<Button
variant={destructive ? 'destructive' : 'primary'}
onPress={onConfirm}
>
Confirm
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
Ref Forwarding
All Hyena components forward refs to their underlying element:
const buttonRef = useRef(null)
<Button ref={buttonRef}>Click me</Button>
// Later
buttonRef.current?.focus()
This enables:
- Imperative focus management
- Measuring layout
- Integration with third-party libraries