Guides/

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?

  1. Flexibility — Rearrange, hide, or customize any part
  2. Composition — Add your own components between parts
  3. Context — State is shared automatically between parts
  4. 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

NamePurpose
TriggerElement that opens/activates the component
ContentThe main content area (popover, modal, etc.)
HeaderHeader section of content
FooterFooter section of content
TitleTitle text with proper heading semantics
DescriptionDescription text
CloseClose button/trigger
ItemIndividual item in a list/menu
OverlayBackground 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

HookReturns
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:

VariantUse Case
primaryMain action, prominent blue styling
secondarySecondary action, subtle styling
ghostMinimal styling, toolbars, subtle actions
destructiveDangerous actions, red styling

Size Variants:

SizeHeightUse Case
sm32pxCompact UI, toolbars
md40pxDefault, forms
lg48pxProminent 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:

ComponentValue PropChange Handler
InputvalueonChangeText
TextareavalueonChangeText
CheckboxcheckedonCheckedChange
SwitchcheckedonCheckedChange
SelectvalueonValueChange
SlidervalueonValueChange
DatePickervalueonValueChange
TimePickervalueonValueChange
RadioGroupvalueonValueChange
TabsvalueonValueChange
AccordionvalueonValueChange

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-disabled for 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