Styling
I want to write styles fast, keep them co-located, and never end up grepping a global CSS file at 2am wondering which rule wins. That’s it. Everything below follows from that.
On web: Tailwind CSS
Utility-first wins for me because it solves the three problems I actually have:
- Co-location — the styles are right there in the component, not three folders away
- No naming — I never have to invent
<div className="card__inner--highlighted-v2"> - Predictable specificity — utilities are flat; there’s no cascade puzzle
export function StatCard({ label, value }: Props) {
return (
<div className="rounded-xl border border-slate-800 bg-slate-900/60 p-5 transition hover:border-sky-500/50">
<p className="text-xs uppercase tracking-wider text-slate-400">{label}</p>
<p className="mt-1 text-2xl font-semibold text-slate-100">{value}</p>
</div>
);
}
What I add on top of Tailwind
clsxorcnhelper — for conditional classes. Never string concatenation.- Design tokens in
tailwind.config.ts— colors, spacing scale, radii. I don’t sprinkle hex codes in components. @applyis allowed, but rarely — only for repeated patterns that genuinely deserve a name (e.g.,.btn-primary). If I’m doing it three times, I extract a component first.
On React Native: StyleSheet or NativeWind
The style prop in RN accepts an object, not classes. Two paths:
StyleSheet — the safe default
import { StyleSheet, View, Text } from "react-native";
export function StatCard({ label, value }: Props) {
return (
<View style={styles.card}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.value}>{value}</Text>
</View>
);
}
const styles = StyleSheet.create({
card: {
borderRadius: 12,
backgroundColor: "rgba(15, 23, 42, 0.6)",
padding: 20,
},
label: { fontSize: 12, color: "#94a3b8", letterSpacing: 0.4 },
value: { fontSize: 22, fontWeight: "600", color: "#f1f5f9", marginTop: 4 },
});
NativeWind — when the web and app share a design system
If a project has both web (Tailwind) and mobile (RN), I reach for NativeWind v5 so the same utility classes work on both. It cuts down on the cognitive tax of switching between platforms in the same codebase.
For RN-only projects, I stay on StyleSheet. Less magic, fewer build-time surprises.
Tokens, not raw values
Whether it’s Tailwind config or a TypeScript theme object, every color/spacing/radius/font-size lives in one tokens file:
export const colors = {
brand: { 500: "#0ea5e9", 600: "#0284c7" },
surface: { base: "#0b1020", raised: "#0f1530" },
text: { primary: "#f1f5f9", muted: "#94a3b8" },
} as const;
Components reference colors.brand[500], never "#0ea5e9". When the design changes, I update one file.
What I avoid
- CSS-in-JS at runtime (styled-components, Emotion) — the perf cost on RN and the cascade footguns on web don’t pay back what I get for them
- Global CSS files beyond a tiny
reset/tokens/typographylayer - Inline
style={{ ... }}for anything reusable — fine for true one-offs, but if I’m doing it twice, it goes into a class or aStyleSheet - Theme prop drilling — themes live in a context (web) or a Zustand slice (RN), never as a prop on every component
Dark mode
I design dark-first. Light mode is a variant, not the default I retrofit later. On web, that means CSS variables driven by a data-theme attribute on <html>; on RN, it’s a useColorScheme() + token mapping. Either way, components never branch on theme — they just consume tokens that already resolved.