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:
background,foreground- the base surface and its text.muted,muted-foreground- a subordinate surface (think sidebar hover).accent,accent-foreground- the highlight color.primary,primary-foreground,secondary,secondary-foreground- for filled buttons and accents.destructive,destructive-foreground- red/danger.border,input,ring- form affordances and outlines.
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:
- Props that take a literal color (
<Search color={fg} />,<RefreshControl tintColor={…} />,shadowColor, gradient stops). - Style-callback APIs that don’t accept
className(e.g.Pressable’sstyle={({ pressed }) => …}). - Reanimated styles where the worklet needs a JS string.
- Composing colors with opacity in template strings (often
bg-primary/10etc. cover this — try the class first).
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.