forked from
esb.lol/alf
Bluesky's "Application Layout Framework"
1# API Reference
2
3Complete reference for `@bsky.app/alf`. All exports come from a single entry point:
4
5```typescript
6import {
7 atoms,
8 useTheme,
9 Provider,
10 Context,
11 tokens,
12 utils,
13 themes,
14 createTheme,
15 createThemes,
16 DEFAULT_PALETTE,
17 DEFAULT_SUBDUED_PALETTE,
18 invertPalette,
19 isWeb,
20 isNative,
21 isIOS,
22 isAndroid,
23 isFabric,
24 web,
25 native,
26 ios,
27 android,
28 platform,
29} from '@bsky.app/alf'
30
31import type {
32 Palette,
33 Theme,
34 ThemeScheme,
35 ThemeName,
36 ThemeAtoms,
37 TextStyleProp,
38 ViewStyleProp,
39} from '@bsky.app/alf'
40```
41
42---
43
44## Provider and Hook
45
46### `Provider`
47
48React context provider that makes the active theme available to all children via `useTheme()`.
49
50```tsx
51<Provider activeTheme="light" themes={themes}>
52 <App />
53</Provider>
54```
55
56| Prop | Type | Description |
57|------|------|-------------|
58| `activeTheme` | `T extends string` | Key into the `themes` object |
59| `themes` | `Record<T, Theme>` | Map of theme name to `Theme` object |
60| `children` | `ReactNode` | Your app |
61
62The provider memoizes the context value based on `activeTheme` and `themes`.
63
64### `useTheme()`
65
66Returns the active `Theme` object from context. You access theme-aware color atoms from `t.atoms`:
67
68```tsx
69const t = useTheme()
70<View style={[t.atoms.bg, t.atoms.shadow_md]}>
71 <Text style={[t.atoms.text]}>Themed text</Text>
72</View>
73```
74
75Returns: `Theme`
76
77### `Context`
78
79The raw React context (`React.Context<{ theme: Theme }>`). Initial value uses `themes.light`. Display name: `'AlfContext'`.
80
81You rarely need this directly. Use `useTheme()` instead.
82
83---
84
85## Atoms
86
87All atoms live on the `atoms` object. They are frozen, static style objects that you combine via arrays.
88
89```tsx
90import { atoms as a } from '@bsky.app/alf'
91
92<View style={[a.flex_row, a.gap_md, a.p_lg, a.rounded_md]} />
93```
94
95### Naming conventions
96
97- Underscore prefix for sizes starting with a number: `_2xs`, `_2xl`, `_3xl`, etc.
98- Axis suffixes: `x` (left + right), `y` (top + bottom)
99- Side suffixes: `t` (top), `b` (bottom), `l` (left), `r` (right)
100
101### Debug
102
103| Atom | Style |
104|------|-------|
105| `debug` | `borderColor: 'red', borderWidth: 1` |
106
107### Positioning
108
109| Atom | Style |
110|------|-------|
111| `fixed` | `position: 'fixed'` (native: `'absolute'`) |
112| `absolute` | `position: 'absolute'` |
113| `relative` | `position: 'relative'` |
114| `static` | `position: 'static'` |
115| `sticky` | `position: 'sticky'` (native: empty) |
116| `inset_0` | `top/right/bottom/left: 0` |
117| `top_0` | `top: 0` |
118| `right_0` | `right: 0` |
119| `bottom_0` | `bottom: 0` |
120| `left_0` | `left: 0` |
121
122### Z-index
123
124| Atom | Value |
125|------|-------|
126| `z_10` | `zIndex: 10` |
127| `z_20` | `zIndex: 20` |
128| `z_30` | `zIndex: 30` |
129| `z_40` | `zIndex: 40` |
130| `z_50` | `zIndex: 50` |
131
132### Overflow
133
134| Atom | Style |
135|------|-------|
136| `overflow_visible` | `overflow: 'visible'` |
137| `overflow_hidden` | `overflow: 'hidden'` |
138| `overflow_auto` | `overflow: 'auto'` (native: empty) |
139| `overflow_x_visible` | `overflowX: 'visible'` |
140| `overflow_x_hidden` | `overflowX: 'hidden'` |
141| `overflow_y_visible` | `overflowY: 'visible'` |
142| `overflow_y_hidden` | `overflowY: 'hidden'` |
143
144### Width and height
145
146| Atom | Style |
147|------|-------|
148| `w_full` | `width: '100%'` |
149| `h_full` | `height: '100%'` |
150| `h_full_vh` | `height: '100vh'` |
151| `max_w_full` | `maxWidth: '100%'` |
152| `max_h_full` | `maxHeight: '100%'` |
153
154### Border radius
155
156Values come from `tokens.borderRadius` (zero variants use literal `0`).
157
158| Atom | Value (px) |
159|------|------------|
160| `rounded_0` | 0 |
161| `rounded_2xs` | 2 |
162| `rounded_xs` | 4 |
163| `rounded_sm` | 8 |
164| `rounded_md` | 12 |
165| `rounded_lg` | 16 |
166| `rounded_xl` | 20 |
167| `rounded_full` | 999 |
168
169### Flexbox
170
171| Atom | Style |
172|------|-------|
173| `flex` | `display: 'flex'` |
174| `flex_col` | `flexDirection: 'column'` |
175| `flex_row` | `flexDirection: 'row'` |
176| `flex_col_reverse` | `flexDirection: 'column-reverse'` |
177| `flex_row_reverse` | `flexDirection: 'row-reverse'` |
178| `flex_wrap` | `flexWrap: 'wrap'` |
179| `flex_nowrap` | `flexWrap: 'nowrap'` |
180| `flex_0` | `flex: '0 0 auto'` (native: `flex: 0`) |
181| `flex_1` | `flex: 1` |
182| `flex_grow` | `flexGrow: 1` |
183| `flex_grow_0` | `flexGrow: 0` |
184| `flex_shrink` | `flexShrink: 1` |
185| `flex_shrink_0` | `flexShrink: 0` |
186
187### Alignment
188
189| Atom | Style |
190|------|-------|
191| `justify_start` | `justifyContent: 'flex-start'` |
192| `justify_center` | `justifyContent: 'center'` |
193| `justify_between` | `justifyContent: 'space-between'` |
194| `justify_end` | `justifyContent: 'flex-end'` |
195| `align_start` | `alignItems: 'flex-start'` |
196| `align_center` | `alignItems: 'center'` |
197| `align_end` | `alignItems: 'flex-end'` |
198| `align_baseline` | `alignItems: 'baseline'` |
199| `align_stretch` | `alignItems: 'stretch'` |
200| `self_auto` | `alignSelf: 'auto'` |
201| `self_start` | `alignSelf: 'flex-start'` |
202| `self_end` | `alignSelf: 'flex-end'` |
203| `self_center` | `alignSelf: 'center'` |
204| `self_stretch` | `alignSelf: 'stretch'` |
205| `self_baseline` | `alignSelf: 'baseline'` |
206
207### Gap
208
209Values come from `tokens.space` (zero variants use literal `0`).
210
211| Atom | Value (px) |
212|------|------------|
213| `gap_0` | 0 |
214| `gap_2xs` | 2 |
215| `gap_xs` | 4 |
216| `gap_sm` | 8 |
217| `gap_md` | 12 |
218| `gap_lg` | 16 |
219| `gap_xl` | 20 |
220| `gap_2xl` | 24 |
221| `gap_3xl` | 28 |
222| `gap_4xl` | 32 |
223| `gap_5xl` | 40 |
224
225### Typography
226
227#### Font size
228
229Each atom sets both `fontSize` and `letterSpacing: 0`. Values come from `tokens.fontSize`.
230
231| Atom | Size (px) |
232|------|-----------|
233| `text_2xs` | 9.4 |
234| `text_xs` | 11.3 |
235| `text_sm` | 13.1 |
236| `text_md` | 15 |
237| `text_lg` | 16.9 |
238| `text_xl` | 18.8 |
239| `text_2xl` | 20.6 |
240| `text_3xl` | 24.3 |
241| `text_4xl` | 30 |
242| `text_5xl` | 37.5 |
243
244#### Text alignment
245
246| Atom | Style |
247|------|-------|
248| `text_left` | `textAlign: 'left'` |
249| `text_center` | `textAlign: 'center'` |
250| `text_right` | `textAlign: 'right'` |
251
252#### Line height
253
254Values are unitless multipliers from `tokens.lineHeight`. Use the `utils.leading()` function when you need computed pixel values on native.
255
256| Atom | Multiplier |
257|------|------------|
258| `leading_tight` | 1.15 |
259| `leading_snug` | 1.3 |
260| `leading_relaxed` | 1.5 |
261| `leading_normal` | 1.5 (deprecated, use `leading_relaxed`) |
262
263#### Letter spacing
264
265| Atom | Style |
266|------|-------|
267| `tracking_normal` | `letterSpacing: 0` |
268
269#### Font weight
270
271| Atom | Weight |
272|------|--------|
273| `font_normal` | `'400'` |
274| `font_medium` | `'500'` |
275| `font_semi_bold` | `'600'` |
276| `font_bold` | `'700'` |
277
278#### Font style
279
280| Atom | Style |
281|------|-------|
282| `italic` | `fontStyle: 'italic'` |
283
284### Borders
285
286On native, all 1px border atoms use `StyleSheet.hairlineWidth` instead. See the [platform behavior](#native-overrides) section.
287
288#### Border width
289
290| Atom | Sides | Width |
291|------|-------|-------|
292| `border_0` | All | 0 |
293| `border` | All | 1 |
294| `border_t_0` / `border_t` | Top | 0 / 1 |
295| `border_b_0` / `border_b` | Bottom | 0 / 1 |
296| `border_l_0` / `border_l` | Left | 0 / 1 |
297| `border_r_0` / `border_r` | Right | 0 / 1 |
298| `border_x_0` / `border_x` | Left + Right | 0 / 1 |
299| `border_y_0` / `border_y` | Top + Bottom | 0 / 1 |
300
301#### Border color
302
303| Atom | Style |
304|------|-------|
305| `border_transparent` | `borderColor: 'transparent'` |
306
307For theme-aware border colors, use `t.atoms.border_contrast_low`, `t.atoms.border_contrast_medium`, or `t.atoms.border_contrast_high`.
308
309### Border curves (iOS only)
310
311These resolve to empty objects `{}` on web. On native, they use the `ios()` platform selector, returning the style on iOS and `undefined` on Android.
312
313| Atom | iOS Style |
314|------|-----------|
315| `curve_circular` | `borderCurve: 'circular'` |
316| `curve_continuous` | `borderCurve: 'continuous'` |
317
318### Shadows
319
320Static shadow atoms are empty objects on web and Fabric. Use theme shadow atoms (`t.atoms.shadow_sm`, etc.) for cross-platform shadows with proper theme colors.
321
322| Atom | Description |
323|------|-------------|
324| `shadow_sm` | Small shadow (native only, disabled on Fabric) |
325| `shadow_md` | Medium shadow (native only, disabled on Fabric) |
326| `shadow_lg` | Large shadow (native only, disabled on Fabric) |
327
328### Gutters
329
330Semantic padding shortcuts. Each has `_x` (horizontal) and `_y` (vertical) variants.
331
332| Atom | Padding (px) |
333|------|-------------|
334| `gutter_tight` / `gutter_x_tight` / `gutter_y_tight` | 8 |
335| `gutter_snug` / `gutter_x_snug` / `gutter_y_snug` | 12 |
336| `gutter_default` / `gutter_x_default` / `gutter_y_default` | 16 |
337| `gutter_wide` / `gutter_x_wide` / `gutter_y_wide` | 20 |
338| `gutter_extra_wide` / `gutter_x_extra_wide` / `gutter_y_extra_wide` | 24 |
339
340### Padding
341
342Values come from `tokens.space` (zero variants use literal `0`). Each size has `p_`, `px_`, `py_`, `pt_`, `pb_`, `pl_`, `pr_` variants.
343
344| Size | Value (px) |
345|------|------------|
346| `0` | 0 |
347| `2xs` | 2 |
348| `xs` | 4 |
349| `sm` | 8 |
350| `md` | 12 |
351| `lg` | 16 |
352| `xl` | 20 |
353| `2xl` | 24 |
354| `3xl` | 28 |
355| `4xl` | 32 |
356| `5xl` | 40 |
357
358Full atom names follow the pattern: `p_md`, `px_lg`, `py_sm`, `pt_xl`, `pb_2xl`, `pl_xs`, `pr_3xl`.
359
360### Margin
361
362Values come from `tokens.space` (zero variants use literal `0`). Each size has `m_`, `mx_`, `my_`, `mt_`, `mb_`, `ml_`, `mr_` variants.
363
364| Size | Value (px) |
365|------|------------|
366| `0` | 0 |
367| `2xs` | 2 |
368| `xs` | 4 |
369| `sm` | 8 |
370| `md` | 12 |
371| `lg` | 16 |
372| `xl` | 20 |
373| `2xl` | 24 |
374| `3xl` | 28 |
375| `4xl` | 32 |
376| `5xl` | 40 |
377
378Full atom names follow the pattern: `m_md`, `mx_lg`, `my_sm`, `mt_xl`, `mb_2xl`, `ml_xs`, `mr_3xl`.
379
380Auto margins: `m_auto`, `mx_auto`, `my_auto`, `mt_auto`, `mb_auto`, `ml_auto`, `mr_auto`.
381
382### Pointer events and user select
383
384| Atom | Style |
385|------|-------|
386| `pointer_events_none` | `pointerEvents: 'none'` |
387| `pointer_events_auto` | `pointerEvents: 'auto'` |
388| `pointer_events_box_only` | `pointerEvents: 'box-only'` |
389| `pointer_events_box_none` | `pointerEvents: 'box-none'` |
390| `user_select_none` | `userSelect: 'none'` |
391| `user_select_text` | `userSelect: 'text'` |
392| `user_select_all` | `userSelect: 'all'` |
393| `outline_inset_1` | `outlineOffset: -1` |
394
395### Text decoration
396
397| Atom | Style |
398|------|-------|
399| `underline` | `textDecorationLine: 'underline'` |
400| `strike_through` | `textDecorationLine: 'line-through'` |
401
402### Display
403
404| Atom | Style | Platform |
405|------|-------|----------|
406| `hidden` | `display: 'none'` | All |
407| `contents` | `display: 'contents'` | All |
408| `inline` | `display: 'inline'` | Web only (native: empty) |
409| `block` | `display: 'block'` | Web only (native: empty) |
410
411### Cursor
412
413| Atom | Style | Platform |
414|------|-------|----------|
415| `pointer` | `cursor: 'pointer'` | Web only (native: empty) |
416
417---
418
419## Theme Atoms
420
421These live on `t.atoms` (where `t = useTheme()`). They adapt to the active theme's color palette.
422
423Theme atoms only cover the **contrast** color scale — the neutral grays used for text, backgrounds, and borders on nearly every component. Primary, positive, and negative colors are more situational, so they are accessed directly from the palette instead:
424
425```tsx
426const t = useTheme()
427
428// Contrast colors → theme atoms
429<View style={[t.atoms.bg, t.atoms.border_contrast_medium]}>
430 <Text style={[t.atoms.text]}>Neutral text</Text>
431</View>
432
433// Primary / positive / negative → palette
434<View style={{ backgroundColor: t.palette.primary_500 }}>
435 <Text style={{ color: t.palette.positive_500 }}>Success</Text>
436</View>
437```
438
439Palette values still adapt to the active theme automatically via `invertPalette()`, so `t.palette.primary_500` returns the correct shade in light, dark, and dim modes.
440
441### Text
442
443| Atom | Palette source |
444|------|---------------|
445| `t.atoms.text` | `contrast_1000` |
446| `t.atoms.text_contrast_low` | `contrast_400` |
447| `t.atoms.text_contrast_medium` | `contrast_700` |
448| `t.atoms.text_contrast_high` | `contrast_900` |
449| `t.atoms.text_inverted` | `contrast_0` |
450
451### Background
452
453| Atom | Palette source |
454|------|---------------|
455| `t.atoms.bg` | `contrast_0` |
456| `t.atoms.bg_contrast_25` | `contrast_25` |
457| `t.atoms.bg_contrast_50` | `contrast_50` |
458| `t.atoms.bg_contrast_100` | `contrast_100` |
459| `t.atoms.bg_contrast_200` | `contrast_200` |
460| `t.atoms.bg_contrast_300` | `contrast_300` |
461| `t.atoms.bg_contrast_400` | `contrast_400` |
462| `t.atoms.bg_contrast_500` | `contrast_500` |
463| `t.atoms.bg_contrast_600` | `contrast_600` |
464| `t.atoms.bg_contrast_700` | `contrast_700` |
465| `t.atoms.bg_contrast_800` | `contrast_800` |
466| `t.atoms.bg_contrast_900` | `contrast_900` |
467| `t.atoms.bg_contrast_950` | `contrast_950` |
468| `t.atoms.bg_contrast_975` | `contrast_975` |
469
470### Border
471
472| Atom | Palette source |
473|------|---------------|
474| `t.atoms.border_contrast_low` | `contrast_100` |
475| `t.atoms.border_contrast_medium` | `contrast_200` |
476| `t.atoms.border_contrast_high` | `contrast_300` |
477
478### Shadow
479
480| Atom | Description |
481|------|-------------|
482| `t.atoms.shadow_sm` | Small shadow with theme-appropriate opacity |
483| `t.atoms.shadow_md` | Medium shadow |
484| `t.atoms.shadow_lg` | Large shadow |
485
486Shadow atoms include native shadow props and a `boxShadow` CSS string for web. Light themes use 0.1 shadow opacity, dark/dim themes use 0.4.
487
488---
489
490## Tokens
491
492Imported as a namespace: `import { tokens } from '@bsky.app/alf'`. All values are pixel-based numbers (no rem/em).
493
494### `tokens.space`
495
496| Key | Value |
497|-----|-------|
498| `_2xs` | 2 |
499| `xs` | 4 |
500| `sm` | 8 |
501| `md` | 12 |
502| `lg` | 16 |
503| `xl` | 20 |
504| `_2xl` | 24 |
505| `_3xl` | 28 |
506| `_4xl` | 32 |
507| `_5xl` | 40 |
508
509### `tokens.fontSize`
510
511| Key | Value |
512|-----|-------|
513| `_2xs` | 9.4 |
514| `xs` | 11.3 |
515| `sm` | 13.1 |
516| `md` | 15 |
517| `lg` | 16.9 |
518| `xl` | 18.8 |
519| `_2xl` | 20.6 |
520| `_3xl` | 24.3 |
521| `_4xl` | 30 |
522| `_5xl` | 37.5 |
523
524### `tokens.lineHeight`
525
526| Key | Value |
527|-----|-------|
528| `tight` | 1.15 |
529| `snug` | 1.3 |
530| `relaxed` | 1.5 |
531
532### `tokens.borderRadius`
533
534| Key | Value |
535|-----|-------|
536| `_2xs` | 2 |
537| `xs` | 4 |
538| `sm` | 8 |
539| `md` | 12 |
540| `lg` | 16 |
541| `xl` | 20 |
542| `full` | 999 |
543
544### `tokens.fontWeight`
545
546| Key | Value |
547|-----|-------|
548| `normal` | `'400'` |
549| `medium` | `'500'` |
550| `semiBold` | `'600'` |
551| `bold` | `'700'` |
552
553### `tokens.labelerColor`
554
555| Key | Value |
556|-----|-------|
557| `purple` | `rgb(105 0 255)` |
558| `purple_dark` | `rgb(83 0 202)` |
559
560### `tokens.TRACKING`
561
562Letter-spacing constant. Value: `0`.
563
564---
565
566## Palette
567
568### `Palette` type
569
570Defines all color values for a theme. Four color families, each with shades:
571
572- **`contrast_*`** (neutrals): 0, 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950, 975, 1000
573- **`primary_*`** (brand blue): 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950, 975
574- **`positive_*`** (green): same scale as primary
575- **`negative_*`** (red): same scale as primary
576
577Plus `white`, `black`, and `like` (pink, `#EC4899`).
578
579All values are CSS color strings.
580
581### `DEFAULT_PALETTE`
582
583The standard light-mode palette. Contrast ranges from `#FFFFFF` (0) to `#000000` (1000). Primary blues range from `#F5F9FF` (25) to `#001533` (975).
584
585### `DEFAULT_SUBDUED_PALETTE`
586
587A softer alternative used by the dim theme. Same structure, lower contrast. The darkest contrast value is `#151D28` instead of pure black.
588
589### `invertPalette(palette)`
590
591Flips **all four** color scales (contrast, primary, positive, negative) for dark mode. Swaps `_0`/`_25` with `_1000`/`_975`, and so on symmetrically. The `_500` center values in each scale remain unchanged. Keeps `white`, `black`, and `like` unchanged.
592
593```typescript
594const darkPalette = invertPalette(DEFAULT_PALETTE)
595```
596
597Returns: `Palette`
598
599---
600
601## Themes
602
603### `Theme` type
604
605```typescript
606type Theme = {
607 scheme: ThemeScheme // 'light' | 'dark'
608 name: ThemeName // 'light' | 'dark' | 'dim'
609 palette: Palette
610 atoms: ThemeAtoms
611}
612```
613
614### `ThemeScheme`
615
616`'light' | 'dark'`
617
618### `ThemeName`
619
620`'light' | 'dark' | 'dim'`
621
622### `themes`
623
624Pre-built theme objects, ready to pass to `Provider`:
625
626```typescript
627themes.light // Light scheme, DEFAULT_PALETTE
628themes.dark // Dark scheme, inverted DEFAULT_PALETTE, 0.4 shadow opacity
629themes.dim // Dark scheme, inverted DEFAULT_SUBDUED_PALETTE, 0.4 shadow opacity
630```
631
632### `createTheme({ scheme, name, palette, options? })`
633
634Builds a `Theme` from a palette.
635
636| Param | Type | Description |
637|-------|------|-------------|
638| `scheme` | `ThemeScheme` | `'light'` or `'dark'` |
639| `name` | `ThemeName` | `'light'`, `'dark'`, or `'dim'` |
640| `palette` | `Palette` | Color palette to use |
641| `options.shadowOpacity` | `number` | Shadow opacity, defaults to `0.1` |
642
643Returns: `Theme`
644
645### `createThemes({ defaultPalette, subduedPalette })`
646
647Builds all three theme variants at once. Inverts the palettes automatically for dark and dim.
648
649| Param | Type | Description |
650|-------|------|-------------|
651| `defaultPalette` | `Palette` | Used for light and dark themes |
652| `subduedPalette` | `Palette` | Used for the dim theme |
653
654Returns: `{ light: Theme, dark: Theme, dim: Theme }`
655
656---
657
658## Platform
659
660### Detection booleans
661
662These resolve at build time via platform-split files (`.native.ts` vs `.ts`).
663
664| Export | Web | iOS | Android |
665|--------|-----|-----|---------|
666| `isWeb` | `true` | `false` | `false` |
667| `isNative` | `false` | `true` | `true` |
668| `isIOS` | `false` | `true` | `false` |
669| `isAndroid` | `false` | `false` | `true` |
670| `isFabric` | Runtime check: `Boolean(global?.nativeFabricUIManager)` |
671
672### Platform selectors
673
674Identity functions that return the value on the matching platform and `undefined` everywhere else.
675
676```typescript
677web({ cursor: 'pointer' }) // returns the object on web, undefined on native
678native({ elevation: 4 }) // returns the object on native, undefined on web
679ios({ borderCurve: 'continuous' })
680android({ elevation: 8 })
681```
682
683### `platform(specifics)`
684
685Works like React Native's `Platform.select()`. On web, returns `specifics.web` or `specifics.default`.
686
687```typescript
688platform({ web: 16, default: 12 })
689```
690
691> On web, this uses `||` (not `??`), so falsy values like `0` or `""` for `specifics.web` will fall through to `specifics.default`.
692
693---
694
695## Utils
696
697Imported as a namespace: `import { utils } from '@bsky.app/alf'`.
698
699### `utils.alpha(color, opacity)`
700
701Converts a color string to a transparent variant at the given opacity (0 to 1).
702
703```typescript
704utils.alpha('#FF0000', 0.5) // '#FF000080'
705utils.alpha('rgb(255, 0, 0)', 0.5) // 'rgba(255, 0, 0, 0.5)'
706utils.alpha('hsl(0, 100%, 50%)', 0.5) // 'hsla(0, 100%, 50%, 0.5)'
707```
708
709Supported formats: `#RGB`, `#RRGGBB`, `rgb()`, `hsl()`. Returns the original color unchanged if the format is not recognized.
710
711### `utils.leading(textStyle)`
712
713Calculates a `lineHeight` value from a text style's `fontSize` and `lineHeight` multiplier.
714
715```typescript
716utils.leading({ fontSize: 15, lineHeight: 1.5 })
717// Web: { lineHeight: '1.5' } (unitless string)
718// Native: { lineHeight: 23 } (rounded pixel value)
719```
720
721Defaults to `tokens.lineHeight.snug` when `lineHeight` is missing. On native, also defaults to `tokens.fontSize.sm` when `fontSize` is missing (web does not use `fontSize`).
722
723Returns: `Pick<TextStyle, 'lineHeight'>`
724
725### `utils.select(name, options)`
726
727Theme-aware value selector. Pass a `ThemeName` and an object mapping theme names to values.
728
729```typescript
730utils.select('dark', {
731 light: '#FFFFFF',
732 dark: '#000000',
733 dim: '#1A1A2E',
734})
735// Returns '#000000'
736```
737
738The type signature accepts either an exhaustive map of all three theme names, or a partial map with a `default` key. When using `default`, you still need to provide values for every theme name you want to handle — the `default` value only applies when the `name` argument falls outside the known `ThemeName` union (which shouldn't happen in practice). If a theme name is omitted from the options, its value will be `undefined`:
739
740```typescript
741utils.select('dim', { light: 'white', default: 'black' })
742// Returns undefined — 'dim' matches the switch case but options.dim is not set
743```
744
745For reliable results, always provide all three theme names:
746
747```typescript
748utils.select('dim', { light: 'white', dark: 'black', dim: 'black' })
749// Returns 'black'
750```
751
752### `utils.flatten(style)`
753
754Merges a style array (or nested arrays) into a single object. Filters out falsy values.
755
756```typescript
757utils.flatten([a.flex_row, a.gap_md, false && a.p_lg])
758// { flexDirection: 'row', gap: 12 }
759```
760
761On web and native, this delegates to `StyleSheet.flatten`. A custom fallback implementation is provided for other environments where neither `.web.ts` nor `.native.ts` is resolved.
762
763Returns: merged style object
764
765---
766
767## Types
768
769### `TextStyleProp`
770
771```typescript
772type TextStyleProp = { style?: StyleProp<TextStyle> }
773```
774
775### `ViewStyleProp`
776
777```typescript
778type ViewStyleProp = { style?: StyleProp<ViewStyle> }
779```
780
781### `ThemeAtoms`
782
783The type of `t.atoms` (where `t = useTheme()`). Maps each theme atom name to its style object.
784
785| Key group | Keys |
786|-----------|------|
787| Text | `text`, `text_contrast_low`, `text_contrast_medium`, `text_contrast_high`, `text_inverted` |
788| Background | `bg`, `bg_contrast_25`, `bg_contrast_50`, `bg_contrast_100` through `bg_contrast_900`, `bg_contrast_950`, `bg_contrast_975` |
789| Border | `border_contrast_low`, `border_contrast_medium`, `border_contrast_high` |
790| Shadow | `shadow_sm`, `shadow_md`, `shadow_lg` |
791
792### `Palette`
793
794Color value map for a theme. See [Palette](#palette) for the full shape.
795
796### `Theme`
797
798Complete theme object containing scheme, name, palette, and atoms. See [Themes](#themes) for details.
799
800### `ThemeScheme`
801
802`'light' | 'dark'`
803
804### `ThemeName`
805
806`'light' | 'dark' | 'dim'`
807
808---
809
810## Native Overrides
811
812When running on iOS or Android, these atoms behave differently from their web counterparts:
813
814| Atom | Web | Native |
815|------|-----|--------|
816| `fixed` | `position: 'fixed'` | `position: 'absolute'` |
817| `sticky` | `position: 'sticky'` | Empty object |
818| `overflow_auto` | `overflow: 'auto'` | Empty object |
819| `flex_0` | `flex: '0 0 auto'` | `flex: 0` |
820| `border`, `border_t`, `border_b`, `border_l`, `border_r`, `border_x`, `border_y` | `borderWidth: 1` | `borderWidth: StyleSheet.hairlineWidth` |
821| `curve_circular` | Empty object | iOS: `borderCurve: 'circular'`, Android: `undefined` |
822| `curve_continuous` | Empty object | iOS: `borderCurve: 'continuous'`, Android: `undefined` |
823| `shadow_sm`, `shadow_md`, `shadow_lg` | Empty object | Shadow props with `elevation` (Fabric: empty) |
824| `inline`, `block` | `display: 'inline'` / `'block'` | Empty object |
825| `pointer` | `cursor: 'pointer'` | Empty object |