Bluesky's "Application Layout Framework"

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, 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.

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 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).

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