Modern CSS styling methodology prioritizing CSS Grid over Flexbox. Covers Grid layout patterns, Subgrid, Container Queries, Design Tokens, and maintainable CSS architecture. Reference for implementing scalable, responsive layouts with predictable behavior.
gap instead of margin utilitiesdata-* attributes for state-based stylingRule: @source must include all packages whose Tailwind classes are used.
When a package (e.g., app) imports components from another package (e.g., @internal/ui), the consuming package's CSS must include for all dependency packages. Otherwise, Tailwind classes in the imported components won't be generated.
@source/* apps/app/src/index.css */
@import "@internal/theme/index.css";
@source "../../../packages/ui/src/**/*.tsx"; /* UI components */
@source "../../../packages/dock/src/**/*.tsx"; /* Dock components */
Common symptom: Styles (padding, colors, etc.) work in Storybook but not in the app—the app's @source is missing the component's package.
Tailwind v4's font-sans class uses --font-sans, but design tokens may define --font-family-sans. Use @theme inline to map them:
@theme inline {
--font-sans: var(--font-family-sans);
--font-mono: var(--font-family-mono);
}
This enables dynamic font switching when themes change (works in both app and Storybook).
Why Grid over Flexbox?
| Aspect | CSS Grid | Flexbox |
|---|---|---|
| Dimensionality | 2D (rows + columns) | 1D (row OR column) |
| Track sizing | Explicit control | Content-driven |
| Alignment | Grid lines provide precise placement | Relies on order/flex properties |
| Nested alignment | Subgrid inherits parent tracks | No inheritance |
| Predictability | Deterministic track-based | Auto-distribution can surprise |
Use Flexbox only for:
justify-content: center; align-items: center).grid-auto {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
gap: var(--spacing-4);
}
Key techniques:
auto-fit collapses empty tracks; auto-fill preserves themminmax() sets flexible boundsmin(100%, 280px) prevents overflow on narrow containers.grid-12 {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: var(--spacing-4);
}
.span-6 { grid-column: span 6; }
.span-4 { grid-column: span 4; }
.col-start-2 { grid-column-start: 2; }
.app-layout {
display: grid;
grid-template-areas:
"header header header"
"sidebar content content"
"footer footer footer";
grid-template-columns: 240px 1fr 1fr;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.content { grid-area: content; }
.footer { grid-area: footer; }
Advantages:
.masonry-like {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-auto-flow: dense;
gap: var(--spacing-2);
}
Subgrid allows child grids to inherit parent track sizing.
.parent-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-4);
}
.card {
display: grid;
grid-template-columns: subgrid; /* Inherit parent columns */
grid-template-rows: auto 1fr auto;
grid-column: span 3;
}
.card-header { grid-column: 1 / -1; }
.card-body { grid-column: 1 / -1; }
.card-footer { grid-column: 1 / -1; }
.item {
display: grid;
grid-template-columns: subgrid; /* Inherit columns */
grid-template-rows: repeat(3, auto); /* Custom rows */
}
.subgrid-item {
grid-template-columns: subgrid;
grid-template-rows: subgrid;
row-gap: 0; /* Override parent gap */
}
Container queries enable styles based on container size, not viewport.
/* 1. Define container */
.card-container {
container-type: inline-size;
container-name: card; /* Optional: named container */
}
/* 2. Query container */
@container card (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 150px 1fr;
}
}
@container card (min-width: 600px) {
.card {
grid-template-columns: 200px 1fr 1fr;
}
}
/* Cleaner range queries */
@container (width >= 300px) { }
@container (200px <= width <= 500px) { }
.responsive-text {
font-size: clamp(1rem, 4cqi, 2rem); /* 4% of container inline size */
}
.container-relative {
padding: 5cqw; /* 5% of container width */
}
| Unit | Description |
|---|---|
cqw | 1% of container width |
cqh | 1% of container height |
cqi | 1% of container inline size |
cqb | 1% of container block size |
/* Width queries only (most common) */
container-type: inline-size;
/* Width AND height queries (requires defined height) */
container-type: size;
Every numeric value in CSS must reference a design token (CSS variable).
/* ❌ BAD: Hardcoded values */
.button {
min-width: 1.5rem;
min-height: 1.5rem;
padding: 0.25rem;
border: 1px solid #e0e0e0;
z-index: 50;
}
/* ✅ GOOD: Design tokens */
.button {
min-width: var(--size-icon-btn);
min-height: var(--size-icon-btn);
padding: var(--spacing-1);
border: var(--spacing-px) solid var(--color-border);
z-index: var(--z-index-modal-backdrop);
}
| Category | Prefix | Examples |
|---|---|---|
| Colors | --color- | --color-primary, --color-background, --color-border |
| Spacing | --spacing- | --spacing-1, --spacing-2, --spacing-px |
| Size | --size- | --size-icon-btn, --size-panel-min, --size-scrollbar |
| Z-Index | --z-index- | --z-index-dropdown, --z-index-modal, --z-index-tooltip |
| Radius | --radius- | --radius-sm, --radius-md, --radius-lg |
| Duration | --duration- | --duration-fast, --duration-normal, --duration-slow |
| Easing | --easing- | --easing-default, --easing-in-out, --easing-spring |
| Shadow | --shadow- | --shadow-sm, --shadow-md, --shadow-lg |
| Font | --font- | --font-family-sans, --font-weight-bold |
/* ❌ BAD: Primitive color in component */
.panel-preview {
background: #d1d5db;
color: #1f2937;
}
/* ✅ GOOD: Semantic color tokens */
.panel-preview {
background: var(--color-popover);
color: var(--color-popover-foreground);
}
Semantic color examples:
--color-background / --color-foreground (base)--color-card / --color-card-foreground--color-popover / --color-popover-foreground--color-primary / --color-primary-foreground--color-muted / --color-muted-foreground--color-destructive / --color-destructive-foreground--color-border / --color-border-subtleNever use margin utilities (mt-*, mb-*, mx-*, my-*) for spacing between elements.
/* ❌ BAD: Margin utilities */
<div className='flex flex-col'>
<h2 className='mb-2'>Title</h2>
<p className='mb-2'>Description</p>
<ul className='mt-2'>
<li className='mb-1'>Item 1</li>
<li className='mb-1'>Item 2</li>
</ul>
</div>
/* ✅ GOOD: Grid with gap */
<div className='grid gap-2'>
<h2>Title</h2>
<p>Description</p>
<ul className='grid gap-1'>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
| Margin Pattern | Grid/Flex Replacement |
|---|---|
flex flex-col + mb-* | grid gap-* |
flex + mr-* | grid grid-flow-col gap-* |
flex items-center + mx-* | grid grid-flow-col auto-cols-max items-center gap-* |
Stacked items with space-y-* | grid gap-* |
Horizontal items with space-x-* | grid grid-flow-col auto-cols-max gap-* |
Padding (p-*, px-*, py-*) remains valid for internal spacing within a component.
/* ✅ OK: Padding for internal spacing */
<button className='px-4 py-2'>Click me</button>
<div className='p-3'>Panel content</div>
/* ❌ BAD: Template literal with conditional classes */
<article
className={`
dock-panel
${state.type === "dragging" ? "opacity-50" : ""}
${isPanelMaximized ? "z-[var(--z-index-modal-backdrop)]" : ""}
`}
>
/* ✅ GOOD: Data attributes */
<article
className='dock-panel'
data-dragging={state.type === "dragging" ? "" : undefined}
data-maximized={isPanelMaximized ? "" : undefined}
>
.dock-panel {
/* Base styles */
&[data-dragging] {
opacity: 0.5;
}
&[data-maximized] {
z-index: var(--z-index-modal-backdrop);
}
}
Only use inline style for truly dynamic values that cannot be expressed as design tokens or CSS.
/* ✅ ACCEPTABLE: Dynamic DOM rect values */
<div
className='drop-indicator'
style={{
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height,
}}
/>
/* ✅ ACCEPTABLE: Dynamic grid template from runtime calculation */
<div
className='grid'
style={{
gridTemplateColumns: `${size * 100}% auto ${(1 - size) * 100}%`,
}}
/>
/* ❌ BAD: Static dimensions */
<div style={{ width: "100%", height: "100%" }}>
/* ✅ GOOD: Tailwind classes */
<div className='w-full h-full'>
/* ❌ BAD: Conditional styling */
<div style={{ opacity: isDragging ? 0.5 : 1 }}>
/* ✅ GOOD: Data attribute + CSS */
<div data-dragging={isDragging ? "" : undefined}>
If a component accepts a style prop only for width: 100% / height: 100%, remove it:
/* ❌ BAD: Unnecessary style prop */
interface Props {
style?: React.CSSProperties
}
const Panel = ({ style }: Props) => (
<div style={style}>...</div>
)
// Usage
<Panel style={{ width: "100%", height: "100%" }} />
/* ✅ GOOD: Built-in full size */
const Panel = () => (
<div className='w-full h-full'>...</div>
)
// Usage
<Panel />
/* ❌ High specificity, hard to override */
.sidebar .nav .nav-item.active a { }
/* ✅ Flat specificity, composable */
.nav-item { }
.nav-item--active { }
/* Block */
.panel { }
/* Element (part of block) */
.panel__header { }
.panel__content { }
.panel__footer { }
/* Modifier (variation) */
.panel--compact { }
.panel--elevated { }
@layer reset, tokens, base, components, utilities;
@layer reset {
*, *::before, *::after { box-sizing: border-box; }
}
@layer tokens {
:root { --color-primary: #0891b2; }
}
@layer base {
body { font-family: var(--font-sans); }
}
@layer components {
.btn { /* component styles */ }
}
@layer utilities {
.sr-only { /* utility overrides */ }
}
/* Component-scoped defaults */
.card {
--card-padding: var(--spacing-4);
--card-radius: var(--radius-md);
padding: var(--card-padding);
border-radius: var(--card-radius);
}
/* Override via inline or parent */
.compact-layout .card {
--card-padding: var(--spacing-2);
}
.grid {
/* Align entire grid within container */
justify-content: center; /* Horizontal */
align-content: center; /* Vertical */
/* Align all items within cells */
justify-items: stretch; /* Horizontal */
align-items: stretch; /* Vertical */
/* Shorthand */
place-content: center; /* justify + align content */
place-items: center; /* justify + align items */
}
.item {
/* Individual item alignment */
justify-self: start;
align-self: end;
place-self: start end;
}
| Value | Behavior |
|---|---|
start | Align to start edge |
end | Align to end edge |
center | Center alignment |
stretch | Fill available space (default) |
space-between | Distribute with edges flush |
space-around | Equal space around items |
space-evenly | Equal space including edges |
/* Fluid grid */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
}
/* Fluid typography */
.heading {
font-size: clamp(1.5rem, 1rem + 2vw, 3rem);
}
/* Fluid spacing */
.section {
padding: clamp(var(--spacing-4), 5vw, var(--spacing-12));
}
.widget-container {
container-type: inline-size;
}
@container (width < 300px) {
.widget { flex-direction: column; }
}
@container (width >= 300px) {
.widget { flex-direction: row; }
}
/* Viewport-dependent layout only */
@media (min-width: 768px) {
.app-layout {
grid-template-areas:
"sidebar header"
"sidebar content";
grid-template-columns: 240px 1fr;
}
}
transform and opacity for animationscontain: layout for isolated components/* Layout containment for performance */
.card {
contain: layout style;
}
/* ⚠️ Grid can reorder visually but not in DOM */
.item { order: -1; } /* Keyboard/screen reader order unchanged */
Rule: Never use CSS ordering to restructure content meaning.
:focus-visible {
outline: 2px solid var(--color-ring);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
| Layout Need | Solution |
|---|---|
| Page structure | Grid with template areas |
| Card grid | auto-fit + minmax() |
| Aligned nested content | Subgrid |
| Component responsiveness | Container queries |
| Single-axis distribution | Grid grid-flow-col auto-cols-max (prefer) or Flexbox |
| Centering | Grid place-items: center |
| Complex alignment | Grid with line placement |
| Vertical stack | grid gap-* (not flex flex-col + margin) |
| Horizontal list | grid grid-flow-col auto-cols-max gap-* |
| Need | Solution |
|---|---|
| Numeric value (size, spacing) | CSS variable from design tokens |
| Color value | Semantic color token (not primitive) |
| Spacing between siblings | Grid/Flex gap-* |
| Internal padding | p-*, px-*, py-* |
| Conditional state styling | data-* attribute + CSS |
| Dynamic position/size | Inline style (only option) |
| Static dimensions | Tailwind classes (w-full h-full) |
| Component style prop | Remove if only used for 100% dimensions |
Position elements relative to other elements without JavaScript:
/* Define anchor */
.trigger {
anchor-name: --tooltip-anchor;
}
/* Position relative to anchor */
.tooltip {
position: fixed;
position-anchor: --tooltip-anchor;
/* Position below the anchor */
top: anchor(bottom);
left: anchor(center);
translate: -50% 0;
/* Fallback positioning */
position-try-fallbacks: flip-block, flip-inline;
}
Use cases: Tooltips, popovers, dropdown menus, annotations
Scoped styling without Shadow DOM:
@scope (.card) to (.card__content) {
/* Styles apply to .card but not descendants inside .card__content */
p { margin-block: 0.5em; }
a { color: var(--color-primary); }
}
/* Scoped to component boundary */
@scope (.component) {
:scope { display: grid; } /* Targets .component itself */
.header { grid-area: header; }
}
Automatic dark mode with single declaration:
:root {
color-scheme: light dark;
}
.card {
background: light-dark(#ffffff, #1a1a1a);
color: light-dark(#1a1a1a, #ffffff);
border: 1px solid light-dark(#e0e0e0, #333333);
}
Enable animation of CSS custom properties:
@property --gradient-angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
.animated-gradient {
background: conic-gradient(from var(--gradient-angle), red, blue, red);
animation: rotate 3s linear infinite;
}
@keyframes rotate {
to { --gradient-angle: 360deg; }
}
Mix colors in any color space:
.button {
--base-color: oklch(50% 0.2 240);
background: var(--base-color);
&:hover {
/* 20% lighter */
background: color-mix(in oklch, var(--base-color), white 20%);
}
&:active {
/* 20% darker */
background: color-mix(in oklch, var(--base-color), black 20%);
}
}
Perceptually uniform color space for consistent lightness:
:root {
/* OKLCH: lightness (0-100%), chroma (0-0.4), hue (0-360) */
--color-primary: oklch(55% 0.25 240); /* Vibrant blue */
--color-primary-light: oklch(75% 0.15 240);
--color-primary-dark: oklch(35% 0.25 240);
/* Generate consistent palette by varying lightness */
--gray-50: oklch(97% 0 0);
--gray-100: oklch(93% 0 0);
--gray-500: oklch(55% 0 0);
--gray-900: oklch(15% 0 0);
}
Benefits:
/* Progressive enhancement pattern */
.component {
/* Baseline */
display: flex;
flex-wrap: wrap;
/* Modern enhancement */
@supports (grid-template-columns: subgrid) {
display: grid;
grid-template-columns: subgrid;
}
@supports (anchor-name: --test) {
/* Use anchor positioning */
}
}
Rule: padding >= border-radius to prevent text clipping at corners.
Descenders (g, y, p, q, j) clip when padding is less than border-radius.
| Border Radius | Min Padding | Example |
|---|---|---|
rounded-md (6px) | p-1.5 (6px) | py-1.5 rounded-md ✅ |
rounded-lg (8px) | p-2 (8px) | py-2 rounded-lg ✅ |
CSS variable escaping: Use var(--spacing-1\.5) (escaped dot) in CSS, p-1.5 in Tailwind.
var(--spacing-*), var(--size-*) etc.var(--color-*) or Tailwind equivalentsvar(--z-index-*) tokensmt-*, mb-*, etc. with grid gap-*grid unless single-axis centering truly requires flexstyle={{ width: "100%", height: "100%" }} → className='w-full h-full'style?: React.CSSProperties if only used for full dimensions&[data-*], not in JSX# Find hardcoded rem/px in CSS
grep -r '\d+\.\d*rem\|\d+px' packages/**/*.css
# Find margin utilities in TSX
grep -r 'm[tblrxy]-\|mt-\|mb-\|ml-\|mr-\|mx-\|my-' packages/**/*.tsx
# Find inline styles in TSX
grep -r 'style=\{\{' packages/**/*.tsx
# Find hardcoded z-index
grep -r 'z-\d\+\|z-\[\d' packages/**/*.tsx packages/**/*.css
# Find hardcoded colors
grep -r '#[0-9a-fA-F]\{3,6\}' packages/**/*.tsx packages/**/*.css