# API Reference Complete reference for `@bsky.app/alf`. All exports come from a single entry point: ```typescript import { atoms, useTheme, Provider, Context, tokens, utils, themes, createTheme, createThemes, DEFAULT_PALETTE, DEFAULT_SUBDUED_PALETTE, invertPalette, isWeb, isNative, isIOS, isAndroid, isFabric, web, native, ios, android, platform, } from '@bsky.app/alf' import type { Palette, Theme, ThemeScheme, ThemeName, ThemeAtoms, TextStyleProp, ViewStyleProp, } from '@bsky.app/alf' ``` --- ## Provider and Hook ### `Provider` React context provider that makes the active theme available to all children via `useTheme()`. ```tsx ``` | Prop | Type | Description | |------|------|-------------| | `activeTheme` | `T extends string` | Key into the `themes` object | | `themes` | `Record` | Map of theme name to `Theme` object | | `children` | `ReactNode` | Your app | The provider memoizes the context value based on `activeTheme` and `themes`. ### `useTheme()` Returns the active `Theme` object from context. You access theme-aware color atoms from `t.atoms`: ```tsx const t = useTheme() Themed text ``` Returns: `Theme` ### `Context` The raw React context (`React.Context<{ theme: Theme }>`). Initial value uses `themes.light`. Display name: `'AlfContext'`. You rarely need this directly. Use `useTheme()` instead. --- ## Atoms All atoms live on the `atoms` object. They are frozen, static style objects that you combine via arrays. ```tsx import { atoms as a } from '@bsky.app/alf' ``` ### Naming conventions - Underscore prefix for sizes starting with a number: `_2xs`, `_2xl`, `_3xl`, etc. - Axis suffixes: `x` (left + right), `y` (top + bottom) - Side suffixes: `t` (top), `b` (bottom), `l` (left), `r` (right) ### Debug | Atom | Style | |------|-------| | `debug` | `borderColor: 'red', borderWidth: 1` | ### Positioning | Atom | Style | |------|-------| | `fixed` | `position: 'fixed'` (native: `'absolute'`) | | `absolute` | `position: 'absolute'` | | `relative` | `position: 'relative'` | | `static` | `position: 'static'` | | `sticky` | `position: 'sticky'` (native: empty) | | `inset_0` | `top/right/bottom/left: 0` | | `top_0` | `top: 0` | | `right_0` | `right: 0` | | `bottom_0` | `bottom: 0` | | `left_0` | `left: 0` | ### Z-index | Atom | Value | |------|-------| | `z_10` | `zIndex: 10` | | `z_20` | `zIndex: 20` | | `z_30` | `zIndex: 30` | | `z_40` | `zIndex: 40` | | `z_50` | `zIndex: 50` | ### Overflow | Atom | Style | |------|-------| | `overflow_visible` | `overflow: 'visible'` | | `overflow_hidden` | `overflow: 'hidden'` | | `overflow_auto` | `overflow: 'auto'` (native: empty) | | `overflow_x_visible` | `overflowX: 'visible'` | | `overflow_x_hidden` | `overflowX: 'hidden'` | | `overflow_y_visible` | `overflowY: 'visible'` | | `overflow_y_hidden` | `overflowY: 'hidden'` | ### Width and height | Atom | Style | |------|-------| | `w_full` | `width: '100%'` | | `h_full` | `height: '100%'` | | `h_full_vh` | `height: '100vh'` | | `max_w_full` | `maxWidth: '100%'` | | `max_h_full` | `maxHeight: '100%'` | ### Border radius Values come from `tokens.borderRadius` (zero variants use literal `0`). | Atom | Value (px) | |------|------------| | `rounded_0` | 0 | | `rounded_2xs` | 2 | | `rounded_xs` | 4 | | `rounded_sm` | 8 | | `rounded_md` | 12 | | `rounded_lg` | 16 | | `rounded_xl` | 20 | | `rounded_full` | 999 | ### Flexbox | Atom | Style | |------|-------| | `flex` | `display: 'flex'` | | `flex_col` | `flexDirection: 'column'` | | `flex_row` | `flexDirection: 'row'` | | `flex_col_reverse` | `flexDirection: 'column-reverse'` | | `flex_row_reverse` | `flexDirection: 'row-reverse'` | | `flex_wrap` | `flexWrap: 'wrap'` | | `flex_nowrap` | `flexWrap: 'nowrap'` | | `flex_0` | `flex: '0 0 auto'` (native: `flex: 0`) | | `flex_1` | `flex: 1` | | `flex_grow` | `flexGrow: 1` | | `flex_grow_0` | `flexGrow: 0` | | `flex_shrink` | `flexShrink: 1` | | `flex_shrink_0` | `flexShrink: 0` | ### Alignment | Atom | Style | |------|-------| | `justify_start` | `justifyContent: 'flex-start'` | | `justify_center` | `justifyContent: 'center'` | | `justify_between` | `justifyContent: 'space-between'` | | `justify_end` | `justifyContent: 'flex-end'` | | `align_start` | `alignItems: 'flex-start'` | | `align_center` | `alignItems: 'center'` | | `align_end` | `alignItems: 'flex-end'` | | `align_baseline` | `alignItems: 'baseline'` | | `align_stretch` | `alignItems: 'stretch'` | | `self_auto` | `alignSelf: 'auto'` | | `self_start` | `alignSelf: 'flex-start'` | | `self_end` | `alignSelf: 'flex-end'` | | `self_center` | `alignSelf: 'center'` | | `self_stretch` | `alignSelf: 'stretch'` | | `self_baseline` | `alignSelf: 'baseline'` | ### Gap Values come from `tokens.space` (zero variants use literal `0`). | Atom | Value (px) | |------|------------| | `gap_0` | 0 | | `gap_2xs` | 2 | | `gap_xs` | 4 | | `gap_sm` | 8 | | `gap_md` | 12 | | `gap_lg` | 16 | | `gap_xl` | 20 | | `gap_2xl` | 24 | | `gap_3xl` | 28 | | `gap_4xl` | 32 | | `gap_5xl` | 40 | ### Typography #### Font size Each atom sets both `fontSize` and `letterSpacing: 0`. Values come from `tokens.fontSize`. | Atom | Size (px) | |------|-----------| | `text_2xs` | 9.4 | | `text_xs` | 11.3 | | `text_sm` | 13.1 | | `text_md` | 15 | | `text_lg` | 16.9 | | `text_xl` | 18.8 | | `text_2xl` | 20.6 | | `text_3xl` | 24.3 | | `text_4xl` | 30 | | `text_5xl` | 37.5 | #### Text alignment | Atom | Style | |------|-------| | `text_left` | `textAlign: 'left'` | | `text_center` | `textAlign: 'center'` | | `text_right` | `textAlign: 'right'` | #### Line height Values are unitless multipliers from `tokens.lineHeight`. Use the `utils.leading()` function when you need computed pixel values on native. | Atom | Multiplier | |------|------------| | `leading_tight` | 1.15 | | `leading_snug` | 1.3 | | `leading_relaxed` | 1.5 | | `leading_normal` | 1.5 (deprecated, use `leading_relaxed`) | #### Letter spacing | Atom | Style | |------|-------| | `tracking_normal` | `letterSpacing: 0` | #### Font weight | Atom | Weight | |------|--------| | `font_normal` | `'400'` | | `font_medium` | `'500'` | | `font_semi_bold` | `'600'` | | `font_bold` | `'700'` | #### Font style | Atom | Style | |------|-------| | `italic` | `fontStyle: 'italic'` | ### Borders On native, all 1px border atoms use `StyleSheet.hairlineWidth` instead. See the [platform behavior](#native-overrides) section. #### Border width | Atom | Sides | Width | |------|-------|-------| | `border_0` | All | 0 | | `border` | All | 1 | | `border_t_0` / `border_t` | Top | 0 / 1 | | `border_b_0` / `border_b` | Bottom | 0 / 1 | | `border_l_0` / `border_l` | Left | 0 / 1 | | `border_r_0` / `border_r` | Right | 0 / 1 | | `border_x_0` / `border_x` | Left + Right | 0 / 1 | | `border_y_0` / `border_y` | Top + Bottom | 0 / 1 | #### Border color | Atom | Style | |------|-------| | `border_transparent` | `borderColor: 'transparent'` | For theme-aware border colors, use `t.atoms.border_contrast_low`, `t.atoms.border_contrast_medium`, or `t.atoms.border_contrast_high`. ### Border curves (iOS only) These resolve to empty objects `{}` on web. On native, they use the `ios()` platform selector, returning the style on iOS and `undefined` on Android. | Atom | iOS Style | |------|-----------| | `curve_circular` | `borderCurve: 'circular'` | | `curve_continuous` | `borderCurve: 'continuous'` | ### Shadows Static 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. | Atom | Description | |------|-------------| | `shadow_sm` | Small shadow (native only, disabled on Fabric) | | `shadow_md` | Medium shadow (native only, disabled on Fabric) | | `shadow_lg` | Large shadow (native only, disabled on Fabric) | ### Gutters Semantic padding shortcuts. Each has `_x` (horizontal) and `_y` (vertical) variants. | Atom | Padding (px) | |------|-------------| | `gutter_tight` / `gutter_x_tight` / `gutter_y_tight` | 8 | | `gutter_snug` / `gutter_x_snug` / `gutter_y_snug` | 12 | | `gutter_default` / `gutter_x_default` / `gutter_y_default` | 16 | | `gutter_wide` / `gutter_x_wide` / `gutter_y_wide` | 20 | | `gutter_extra_wide` / `gutter_x_extra_wide` / `gutter_y_extra_wide` | 24 | ### Padding Values come from `tokens.space` (zero variants use literal `0`). Each size has `p_`, `px_`, `py_`, `pt_`, `pb_`, `pl_`, `pr_` variants. | Size | Value (px) | |------|------------| | `0` | 0 | | `2xs` | 2 | | `xs` | 4 | | `sm` | 8 | | `md` | 12 | | `lg` | 16 | | `xl` | 20 | | `2xl` | 24 | | `3xl` | 28 | | `4xl` | 32 | | `5xl` | 40 | Full atom names follow the pattern: `p_md`, `px_lg`, `py_sm`, `pt_xl`, `pb_2xl`, `pl_xs`, `pr_3xl`. ### Margin Values come from `tokens.space` (zero variants use literal `0`). Each size has `m_`, `mx_`, `my_`, `mt_`, `mb_`, `ml_`, `mr_` variants. | Size | Value (px) | |------|------------| | `0` | 0 | | `2xs` | 2 | | `xs` | 4 | | `sm` | 8 | | `md` | 12 | | `lg` | 16 | | `xl` | 20 | | `2xl` | 24 | | `3xl` | 28 | | `4xl` | 32 | | `5xl` | 40 | Full atom names follow the pattern: `m_md`, `mx_lg`, `my_sm`, `mt_xl`, `mb_2xl`, `ml_xs`, `mr_3xl`. Auto margins: `m_auto`, `mx_auto`, `my_auto`, `mt_auto`, `mb_auto`, `ml_auto`, `mr_auto`. ### Pointer events and user select | Atom | Style | |------|-------| | `pointer_events_none` | `pointerEvents: 'none'` | | `pointer_events_auto` | `pointerEvents: 'auto'` | | `pointer_events_box_only` | `pointerEvents: 'box-only'` | | `pointer_events_box_none` | `pointerEvents: 'box-none'` | | `user_select_none` | `userSelect: 'none'` | | `user_select_text` | `userSelect: 'text'` | | `user_select_all` | `userSelect: 'all'` | | `outline_inset_1` | `outlineOffset: -1` | ### Text decoration | Atom | Style | |------|-------| | `underline` | `textDecorationLine: 'underline'` | | `strike_through` | `textDecorationLine: 'line-through'` | ### Display | Atom | Style | Platform | |------|-------|----------| | `hidden` | `display: 'none'` | All | | `contents` | `display: 'contents'` | All | | `inline` | `display: 'inline'` | Web only (native: empty) | | `block` | `display: 'block'` | Web only (native: empty) | ### Cursor | Atom | Style | Platform | |------|-------|----------| | `pointer` | `cursor: 'pointer'` | Web only (native: empty) | --- ## Theme Atoms These live on `t.atoms` (where `t = useTheme()`). They adapt to the active theme's color palette. Theme 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: ```tsx const t = useTheme() // Contrast colors → theme atoms Neutral text // Primary / positive / negative → palette Success ``` Palette 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. ### Text | Atom | Palette source | |------|---------------| | `t.atoms.text` | `contrast_1000` | | `t.atoms.text_contrast_low` | `contrast_400` | | `t.atoms.text_contrast_medium` | `contrast_700` | | `t.atoms.text_contrast_high` | `contrast_900` | | `t.atoms.text_inverted` | `contrast_0` | ### Background | Atom | Palette source | |------|---------------| | `t.atoms.bg` | `contrast_0` | | `t.atoms.bg_contrast_25` | `contrast_25` | | `t.atoms.bg_contrast_50` | `contrast_50` | | `t.atoms.bg_contrast_100` | `contrast_100` | | `t.atoms.bg_contrast_200` | `contrast_200` | | `t.atoms.bg_contrast_300` | `contrast_300` | | `t.atoms.bg_contrast_400` | `contrast_400` | | `t.atoms.bg_contrast_500` | `contrast_500` | | `t.atoms.bg_contrast_600` | `contrast_600` | | `t.atoms.bg_contrast_700` | `contrast_700` | | `t.atoms.bg_contrast_800` | `contrast_800` | | `t.atoms.bg_contrast_900` | `contrast_900` | | `t.atoms.bg_contrast_950` | `contrast_950` | | `t.atoms.bg_contrast_975` | `contrast_975` | ### Border | Atom | Palette source | |------|---------------| | `t.atoms.border_contrast_low` | `contrast_100` | | `t.atoms.border_contrast_medium` | `contrast_200` | | `t.atoms.border_contrast_high` | `contrast_300` | ### Shadow | Atom | Description | |------|-------------| | `t.atoms.shadow_sm` | Small shadow with theme-appropriate opacity | | `t.atoms.shadow_md` | Medium shadow | | `t.atoms.shadow_lg` | Large shadow | Shadow 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. --- ## Tokens Imported as a namespace: `import { tokens } from '@bsky.app/alf'`. All values are pixel-based numbers (no rem/em). ### `tokens.space` | Key | Value | |-----|-------| | `_2xs` | 2 | | `xs` | 4 | | `sm` | 8 | | `md` | 12 | | `lg` | 16 | | `xl` | 20 | | `_2xl` | 24 | | `_3xl` | 28 | | `_4xl` | 32 | | `_5xl` | 40 | ### `tokens.fontSize` | Key | Value | |-----|-------| | `_2xs` | 9.4 | | `xs` | 11.3 | | `sm` | 13.1 | | `md` | 15 | | `lg` | 16.9 | | `xl` | 18.8 | | `_2xl` | 20.6 | | `_3xl` | 24.3 | | `_4xl` | 30 | | `_5xl` | 37.5 | ### `tokens.lineHeight` | Key | Value | |-----|-------| | `tight` | 1.15 | | `snug` | 1.3 | | `relaxed` | 1.5 | ### `tokens.borderRadius` | Key | Value | |-----|-------| | `_2xs` | 2 | | `xs` | 4 | | `sm` | 8 | | `md` | 12 | | `lg` | 16 | | `xl` | 20 | | `full` | 999 | ### `tokens.fontWeight` | Key | Value | |-----|-------| | `normal` | `'400'` | | `medium` | `'500'` | | `semiBold` | `'600'` | | `bold` | `'700'` | ### `tokens.labelerColor` | Key | Value | |-----|-------| | `purple` | `rgb(105 0 255)` | | `purple_dark` | `rgb(83 0 202)` | ### `tokens.TRACKING` Letter-spacing constant. Value: `0`. --- ## Palette ### `Palette` type Defines all color values for a theme. Four color families, each with shades: - **`contrast_*`** (neutrals): 0, 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950, 975, 1000 - **`primary_*`** (brand blue): 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950, 975 - **`positive_*`** (green): same scale as primary - **`negative_*`** (red): same scale as primary Plus `white`, `black`, and `like` (pink, `#EC4899`). All values are CSS color strings. ### `DEFAULT_PALETTE` The standard light-mode palette. Contrast ranges from `#FFFFFF` (0) to `#000000` (1000). Primary blues range from `#F5F9FF` (25) to `#001533` (975). ### `DEFAULT_SUBDUED_PALETTE` A softer alternative used by the dim theme. Same structure, lower contrast. The darkest contrast value is `#151D28` instead of pure black. ### `invertPalette(palette)` Flips **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. ```typescript const darkPalette = invertPalette(DEFAULT_PALETTE) ``` Returns: `Palette` --- ## Themes ### `Theme` type ```typescript type Theme = { scheme: ThemeScheme // 'light' | 'dark' name: ThemeName // 'light' | 'dark' | 'dim' palette: Palette atoms: ThemeAtoms } ``` ### `ThemeScheme` `'light' | 'dark'` ### `ThemeName` `'light' | 'dark' | 'dim'` ### `themes` Pre-built theme objects, ready to pass to `Provider`: ```typescript themes.light // Light scheme, DEFAULT_PALETTE themes.dark // Dark scheme, inverted DEFAULT_PALETTE, 0.4 shadow opacity themes.dim // Dark scheme, inverted DEFAULT_SUBDUED_PALETTE, 0.4 shadow opacity ``` ### `createTheme({ scheme, name, palette, options? })` Builds a `Theme` from a palette. | Param | Type | Description | |-------|------|-------------| | `scheme` | `ThemeScheme` | `'light'` or `'dark'` | | `name` | `ThemeName` | `'light'`, `'dark'`, or `'dim'` | | `palette` | `Palette` | Color palette to use | | `options.shadowOpacity` | `number` | Shadow opacity, defaults to `0.1` | Returns: `Theme` ### `createThemes({ defaultPalette, subduedPalette })` Builds all three theme variants at once. Inverts the palettes automatically for dark and dim. | Param | Type | Description | |-------|------|-------------| | `defaultPalette` | `Palette` | Used for light and dark themes | | `subduedPalette` | `Palette` | Used for the dim theme | Returns: `{ light: Theme, dark: Theme, dim: Theme }` --- ## Platform ### Detection booleans These resolve at build time via platform-split files (`.native.ts` vs `.ts`). | Export | Web | iOS | Android | |--------|-----|-----|---------| | `isWeb` | `true` | `false` | `false` | | `isNative` | `false` | `true` | `true` | | `isIOS` | `false` | `true` | `false` | | `isAndroid` | `false` | `false` | `true` | | `isFabric` | Runtime check: `Boolean(global?.nativeFabricUIManager)` | ### Platform selectors Identity functions that return the value on the matching platform and `undefined` everywhere else. ```typescript web({ cursor: 'pointer' }) // returns the object on web, undefined on native native({ elevation: 4 }) // returns the object on native, undefined on web ios({ borderCurve: 'continuous' }) android({ elevation: 8 }) ``` ### `platform(specifics)` Works like React Native's `Platform.select()`. On web, returns `specifics.web` or `specifics.default`. ```typescript platform({ web: 16, default: 12 }) ``` > On web, this uses `||` (not `??`), so falsy values like `0` or `""` for `specifics.web` will fall through to `specifics.default`. --- ## Utils Imported as a namespace: `import { utils } from '@bsky.app/alf'`. ### `utils.alpha(color, opacity)` Converts a color string to a transparent variant at the given opacity (0 to 1). ```typescript utils.alpha('#FF0000', 0.5) // '#FF000080' utils.alpha('rgb(255, 0, 0)', 0.5) // 'rgba(255, 0, 0, 0.5)' utils.alpha('hsl(0, 100%, 50%)', 0.5) // 'hsla(0, 100%, 50%, 0.5)' ``` Supported formats: `#RGB`, `#RRGGBB`, `rgb()`, `hsl()`. Returns the original color unchanged if the format is not recognized. ### `utils.leading(textStyle)` Calculates a `lineHeight` value from a text style's `fontSize` and `lineHeight` multiplier. ```typescript utils.leading({ fontSize: 15, lineHeight: 1.5 }) // Web: { lineHeight: '1.5' } (unitless string) // Native: { lineHeight: 23 } (rounded pixel value) ``` Defaults 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`). Returns: `Pick` ### `utils.select(name, options)` Theme-aware value selector. Pass a `ThemeName` and an object mapping theme names to values. ```typescript utils.select('dark', { light: '#FFFFFF', dark: '#000000', dim: '#1A1A2E', }) // Returns '#000000' ``` The 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`: ```typescript utils.select('dim', { light: 'white', default: 'black' }) // Returns undefined — 'dim' matches the switch case but options.dim is not set ``` For reliable results, always provide all three theme names: ```typescript utils.select('dim', { light: 'white', dark: 'black', dim: 'black' }) // Returns 'black' ``` ### `utils.flatten(style)` Merges a style array (or nested arrays) into a single object. Filters out falsy values. ```typescript utils.flatten([a.flex_row, a.gap_md, false && a.p_lg]) // { flexDirection: 'row', gap: 12 } ``` On 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. Returns: merged style object --- ## Types ### `TextStyleProp` ```typescript type TextStyleProp = { style?: StyleProp } ``` ### `ViewStyleProp` ```typescript type ViewStyleProp = { style?: StyleProp } ``` ### `ThemeAtoms` The type of `t.atoms` (where `t = useTheme()`). Maps each theme atom name to its style object. | Key group | Keys | |-----------|------| | Text | `text`, `text_contrast_low`, `text_contrast_medium`, `text_contrast_high`, `text_inverted` | | Background | `bg`, `bg_contrast_25`, `bg_contrast_50`, `bg_contrast_100` through `bg_contrast_900`, `bg_contrast_950`, `bg_contrast_975` | | Border | `border_contrast_low`, `border_contrast_medium`, `border_contrast_high` | | Shadow | `shadow_sm`, `shadow_md`, `shadow_lg` | ### `Palette` Color value map for a theme. See [Palette](#palette) for the full shape. ### `Theme` Complete theme object containing scheme, name, palette, and atoms. See [Themes](#themes) for details. ### `ThemeScheme` `'light' | 'dark'` ### `ThemeName` `'light' | 'dark' | 'dim'` --- ## Native Overrides When running on iOS or Android, these atoms behave differently from their web counterparts: | Atom | Web | Native | |------|-----|--------| | `fixed` | `position: 'fixed'` | `position: 'absolute'` | | `sticky` | `position: 'sticky'` | Empty object | | `overflow_auto` | `overflow: 'auto'` | Empty object | | `flex_0` | `flex: '0 0 auto'` | `flex: 0` | | `border`, `border_t`, `border_b`, `border_l`, `border_r`, `border_x`, `border_y` | `borderWidth: 1` | `borderWidth: StyleSheet.hairlineWidth` | | `curve_circular` | Empty object | iOS: `borderCurve: 'circular'`, Android: `undefined` | | `curve_continuous` | Empty object | iOS: `borderCurve: 'continuous'`, Android: `undefined` | | `shadow_sm`, `shadow_md`, `shadow_lg` | Empty object | Shadow props with `elevation` (Fabric: empty) | | `inline`, `block` | `display: 'inline'` / `'block'` | Empty object | | `pointer` | `cursor: 'pointer'` | Empty object |