Theming

TinyCld ships a light and a dark theme, and users can flip between them at runtime. Every color in a package needs to come from a semantic token - never a raw hex. Two APIs cover every case: Tailwind class names for JSX, and useThemeColor 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 core set of tokens follows shadcn/ui conventions and is available throughout:

Both themes define every token. Your JSX renders correctly in either - you don’t write className="dark:bg-slate-900 bg-white", you write className="bg-background" and the theme decides.

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 returns the current token’s resolved color and re-renders the component when the theme switches. It accepts any of the tokens listed above.

Prefer className when it works

useThemeColor is a fallback, not the default. When both styles compile to 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 }}:

// 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, the fix is to add a token, not to inline a hex. Tokens are defined in the app shell’s global.css and tailwind.config.js - talk to whoever owns design before adding one.

Don’t hardcode the mode

Both themes are first-class. Don’t write components that assume dark mode (everything except the dock and most screens use both), 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 the same hook:

import { useAppTheme } from '@tinycld/core/lib/use-app-theme'

const theme = useAppTheme() // 'light' | 'dark'

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