API Reference#
Complete reference for @bsky.app/alf. All exports come from a single entry point:
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().
<Provider activeTheme="light" themes={themes}>
<App />
</Provider>
| Prop | Type | Description |
|---|---|---|
activeTheme |
T extends string |
Key into the themes object |
themes |
Record<T, Theme> |
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:
const t = useTheme()
<View style={[t.atoms.bg, t.atoms.shadow_md]}>
<Text style={[t.atoms.text]}>Themed text</Text>
</View>
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.
import { atoms as a } from '@bsky.app/alf'
<View style={[a.flex_row, a.gap_md, a.p_lg, a.rounded_md]} />
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 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:
const t = useTheme()
// Contrast colors → theme atoms
<View style={[t.atoms.bg, t.atoms.border_contrast_medium]}>
<Text style={[t.atoms.text]}>Neutral text</Text>
</View>
// Primary / positive / negative → palette
<View style={{ backgroundColor: t.palette.primary_500 }}>
<Text style={{ color: t.palette.positive_500 }}>Success</Text>
</View>
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, 1000primary_*(brand blue): 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950, 975positive_*(green): same scale as primarynegative_*(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.
const darkPalette = invertPalette(DEFAULT_PALETTE)
Returns: Palette
Themes#
Theme type#
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:
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.
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.
platform({ web: 16, default: 12 })
On web, this uses
||(not??), so falsy values like0or""forspecifics.webwill fall through tospecifics.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).
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.
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<TextStyle, 'lineHeight'>
utils.select(name, options)#
Theme-aware value selector. Pass a ThemeName and an object mapping theme names to values.
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:
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:
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.
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#
type TextStyleProp = { style?: StyleProp<TextStyle> }
ViewStyleProp#
type ViewStyleProp = { style?: StyleProp<ViewStyle> }
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 for the full shape.
Theme#
Complete theme object containing scheme, name, palette, and atoms. See 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 |