Theming

TinyCld ships a light and a dark theme, and users can flip between them at runtime (and a separate “color theme” picker tints the accent palette on top of that). Every color in a package needs to come from a semantic token — never a raw hex. Two APIs cover every case: Tailwind class names with semantic tokens for JSX, and useThemeColor from @tinycld/core/lib/use-app-theme for places JSX isn’t an option.

Semantic tokens in JSX

Prefer Tailwind class names with semantic tokens:

<View className="bg-background border-border rounded-xl p-4">
    <Text className="text-foreground font-semibold">Title</Text>
    <Text className="text-muted-foreground text-sm">Subtitle</Text>
</View>

The token surface lives in @tinycld/core/lib/use-app-theme.ts as the AppThemeColor union. Both light and dark themes define every token, so JSX written against tokens renders correctly under either.

The names you’ll reach for most often:

A handful of custom tokens are reserved for chrome that doesn’t map to the standard system (rail-background, rail-text, rail-active-text, sidebar-background, active-indicator, hover-background, info, info-foreground, primary, primary-foreground). Most package code doesn’t touch these.

If a design requires a color outside this union, the fix is to add a token to @tinycld/core/lib/use-app-theme.ts and tinycld/global.css, not to inline a hex.

Colors outside of className

Some APIs don’t accept a className: Lucide icons take a color prop, React Native Pressable’s style function is imperative, and a handful of third-party components want a literal hex. For those, use useThemeColor:

import { useThemeColor } from '@tinycld/core/lib/use-app-theme'
import { Search } from 'lucide-react-native'

export function SearchIcon() {
    const fg = useThemeColor('foreground')
    return <Search color={fg} size={20} />
}

useThemeColor reads the CSS variable for the named token and re-renders the component when the theme switches. It accepts any name from the AppThemeColor union.

Prefer className when it works

useThemeColor is a fallback, not the default. When both styles produce the same output, the className form is shorter, declarative, and survives token renames automatically. Use the hook only for the cases that genuinely need a string color value:

The thing to avoid is plumbing a hook value straight into an inline style={{ color: fg }} when the className form would have worked:

// bad - verbose, drifts when tokens are renamed
const fg = useThemeColor('foreground')
const bg = useThemeColor('surface-secondary')
return (
    <View style={{ backgroundColor: bg }}>
        <Text style={{ color: fg }}></Text>
    </View>
)

// good - same result, half the code
return (
    <View className="bg-surface-secondary">
        <Text className="text-foreground"></Text>
    </View>
)

If you find yourself calling useThemeColor only to feed the result into a <View style> or <Text style>, convert it to a className.

What not to do

Never hardcode a hex:

// bad - breaks dark mode, breaks accessibility tweaks,
// drifts out of sync with the design system
<View style={{ backgroundColor: '#ffffff' }} />
<Search color="#1f2937" size={20} />

If a design demands a color that isn’t in the token set, add a token — don’t inline a hex.

Don’t hardcode the mode

Both themes are first-class. Don’t write components that assume dark mode, and don’t build a “white-on-white” hack that only looks right in one theme. If you’re wiring a preview surface that must always be one mode regardless of the user’s pref, wrap it explicitly — but that’s rare enough that you should ask before doing it.

Reading the user’s choice

If you genuinely need to branch on the active theme (say, to pick an asset variant), read it through useThemePreference from @tinycld/core/lib/use-theme-preference:

import { useThemePreference } from '@tinycld/core/lib/use-theme-preference'

const { preference, resolved } = useThemePreference()
// preference: 'system' | 'light' | 'dark'  ← the user's stored choice
// resolved:   'light' | 'dark'              ← the mode actually rendering right now

Use resolved when you need to know which theme is on screen, and preference when you’re rendering the theme-picker UI itself.

Almost all theming concerns are solved by tokens though — reach for the mode only when you’ve confirmed tokens alone can’t express it.