Styling
Styling scales based on the scope of visual reuse, state complexity, and structural requirements.
The Problem
Modern UI development is often plagued by styling extremes. Developers frequently create entire reactive components just to reuse a string of CSS classes. Alternatively, markup becomes bloated with complex utility classes making it unreadable, or styles are scattered across multiple CSS files forcing constant context-switching to understand a single element.
Each styling approach (utilities, raw CSS, inline styles) has specific pros and cons. The goal is not to find "the best" way to style, but to understand which approach solves the problem at hand.
What This Page Covers
This page establishes a definitive Styling Progression. You will learn how to manage styling, starting from simple inline classes, when to graduate to local variables or factories, and when to drop down into plain CSS or inline styles based on the architectural demand.
Theming
Before writing individual styles, you must establish how your application handles design tokens (colors, spacing, typography).
CSS Variables
Use CSS Variables for values that change at runtime, such as light/dark mode, tenant branding, or user-defined themes.
Pros: The browser handles the recalculation natively without requiring a Javascript runtime. Cons: Harder to type-check strictly in Javascript, and can become difficult to track if cascaded deeply without a strict naming convention.
:root {
--color-surface: #ffffff;
--color-border: #e5e7eb;
}
[data-theme="dark"] {
--color-surface: #09090b;
--color-border: #27272a;
}JS Variables
Use JS Variables for static design tokens that define your system but never change based on runtime context, or when strictly integrating with build-time tools.
Pros: Strongly typed, auto-completed in the IDE, and easily shared across non-CSS environments (e.g., passing a hex color to a Canvas chart library).
Cons: Requires a Javascript runtime or build step to inject into the DOM.
export const Tokens = {
radius: {
sm: '0.125rem',
md: '0.375rem',
lg: '0.5rem',
},
zIndex: {
dropdown: 100,
modal: 50,
}
} as const;Inline Classes
When a visual combination is used exactly once, keep it inline. This is your starting point.
<section class="max-w-5xl mx-auto px-6 py-12 flex flex-col gap-8">
<header class="border-b border-gray-200 pb-4">
<h1 class="text-2xl font-semibold tracking-tight">Billing Settings</h1>
</header>
{/* ... */}
</section>Why keep it inline?
Maintaining code is top-down. If you scatter single-use styles into global CSS classes or separate files, you force yourself into back-and-forth scanning between files for zero reusability return.
Local Variable Classes
When a visual combination is repeated on the same page, extract it locally. Do not immediately create a framework component just to share a class string.
Good Practice
Create a local variable with the class combination.
const badgeClass = "inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800";
<div class="flex items-center gap-2">
<span class={badgeClass}>Paid</span>
<span class={badgeClass}>Fulfilled</span>
</div>Bad Practice
Creating a component just to reuse classes.
const Badge = ({ children }) => (
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
{children}
</span>
);Why not global?
Extracting locally prevents repetition without polluting the global namespace or introducing the navigating multiple files problem for a strictly local visual concern.
Local Class Factory
When conditional styling logic gets complex in the same file, extract it to a local function. This prevents your markup from becoming bloated with unreadable inline ternaries.
Good Practice
Extract to a local class factory.
const getInvoiceStyle = (status: InvoiceStatus) => {
switch (status) {
case 'paid': return 'bg-green-50 border-green-200 text-green-900';
case 'overdue': return 'bg-red-50 border-red-200 text-red-900';
default: return 'bg-gray-50 border-gray-200 text-gray-900';
}
};
<div class={`p-4 border rounded-lg ${getInvoiceStyle(invoice.status)}`}>
{invoice.amount}
</div>Bad Practice
Bloated inline ternaries.
<div class={`p-4 border rounded-lg ${invoice.status === 'paid' ? 'bg-green-50 border-green-200 text-green-900' : invoice.status === 'overdue' ? 'bg-red-50 border-red-200 text-red-900' : 'bg-gray-50 border-gray-200 text-gray-900'}`}>
{invoice.amount}
</div>Utility Classes in CSS
When a combination of utility classes is repeated across multiple pages (e.g., standard buttons, form inputs, cards), graduate it to your global CSS by composing the utility classes.
Tailwind @utility
The modern approach for creating custom utilities in Tailwind v4. This registers your composition as a first-class utility in the Tailwind engine.
Pros: Automatically generates all variants (hover:form-input, md:form-input). Respects Tailwind's internal CSS sorting. Cons: Vendor lock-in. The syntax is invalid outside of the Tailwind compiler.
@utility form-input {
@apply w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500;
}<input type="email" class="form-input" placeholder="Enter your email" />Class Selectors
The standard approach for explicit, opt-in global styling when not using the @utility API.
Pros: Standard CSS portability. Best for complex structural rules that you don't intend to combine with Tailwind variants. Cons: No automatic variants. You cannot use md:form-input in your HTML unless you explicitly define a media query or pseudo-class in the CSS.
.form-input {
@apply w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500;
}<input type="email" class="form-input" placeholder="Enter your email" />Attribute Selectors
Use attribute selectors when the style is strictly tied to a semantic HTML attribute or state, avoiding the need to manually toggle classes in Javascript.
.form-input[aria-invalid="true"] {
@apply border-red-500 focus:border-red-500 focus:ring-red-500;
}
.form-input[disabled] {
@apply bg-gray-100 text-gray-500 cursor-not-allowed;
}<input
type="email"
class="form-input"
aria-invalid={state.error ? 'true' : undefined}
disabled={state.submitting}
/>Advanced Selectors
Use advanced selectors (like :has(), :is(), :where()) to style complex component relationships globally without writing custom Javascript logic.
/* Dim a pricing card if it contains an unchecked checkbox */
.pricing-card:has(input[type="checkbox"]:not(:checked)) {
@apply opacity-75 grayscale hover:grayscale-0 transition-all;
}<label class="pricing-card border p-4 block cursor-pointer">
<input type="checkbox" class="sr-only" checked={plan.selected} />
<h4>{plan.name}</h4>
<p>{plan.price}</p>
</label>Plain CSS
Graduate to plain CSS when utility classes fail you. Utilities are excellent for standard visual properties, but they become unreadable or physically impossible when dealing with complex grid layouts, deep pseudo-elements, or multi-step animations.
Class Selectors
When the structural layout is simply too complex to express as a single string of utilities.
Good Practice
Clean plain CSS for complex layouts.
.dashboard-layout {
display: grid;
grid-template-columns: 1fr minmax(auto, 240px) minmax(auto, 320px);
gap: 1rem 1.5rem;
align-items: start;
}Bad Practice
Impossible to read in markup.
<div class="grid grid-cols-[1fr_minmax(auto,240px)_minmax(auto,320px)] gap-x-6 gap-y-4 items-start">Attribute Selectors
When applying plain CSS rules based on precise data states without relying on class toggling.
.data-table-row[data-row-status="archived"] {
text-decoration: line-through;
opacity: 0.6;
}Advanced Selectors
When you need to perform "CSS wizardry" that utility classes cannot target effectively.
/* Styling the thumb of a custom range slider */
.volume-slider::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--color-primary);
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}Inline Style
Use inline style properties only as an escape hatch for highly dynamic, Javascript-driven values that are calculated on the fly. It is impossible to pre-generate CSS classes for every possible value of a continuous input.
Good Practice
Inline styles for continuous or dynamic external values.
<div
class="relative w-full rounded-md border"
style={{
height: `${virtualList.totalHeight}px`,
'--brand-color': user.tenant.primaryColor,
transform: `translate3d(0, ${scroll.y}px, 0)`
}}
>
{/* Virtualized items */}
</div>Bad Practice
Trying to map dynamic values to discrete classes.
<div class={`h-[${calculateHeight()}px] bg-[${user.favoriteColor}]`} />When to use inline styles?
If the value comes from a database, a scroll event, mouse coordinates, or mathematical calculations at runtime, it belongs in an inline style or a locally injected CSS Variable. Everything else belongs in a class.
Learn More
- Static UI — When UI should remain inline, and when it should graduate
- Reactive UI — Presenting reactive data without owning it
- Component — When a concern needs its own state, behavior, and reactivity
- Form Components — User-driven form components with built-in validation