| name | styling |
| description | 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. |
Styling Skill
Core Principles
- Grid-First Layout - Use CSS Grid by default, Flexbox only when appropriate
- Design Token System - All values reference CSS variables (no hardcoded values)
- Semantic Colors - Components use semantic tokens, not primitive colors
- Gap over Margin - Use grid/flex
gapinstead of margin utilities - Data Attributes for States - Use
data-*attributes for state-based styling - Minimal Inline Styles - Only use inline styles for truly dynamic values
Tailwind CSS @source Configuration
Rule: @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 @source for all dependency packages. Otherwise, Tailwind classes in the imported components won't be generated.
/* 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 Font Variable Mapping
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).
Philosophy: Grid-First Layout
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:
- Single-axis alignment (nav items, button groups)
- Content-driven sizing where exact track control isn't needed
- Simple centering (
justify-content: center; align-items: center)
Core Grid Patterns
1. Intrinsic Responsive Grid (No Media Queries)
.grid-auto {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
gap: var(--spacing-4);
}
Key techniques:
auto-fitcollapses empty tracks;auto-fillpreserves themminmax()sets flexible boundsmin(100%, 280px)prevents overflow on narrow containers
2. Explicit Track Grid
.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; }
3. Named Template Areas
.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:
- Visual layout definition in CSS
- Easy responsive redesign by redefining areas
- Self-documenting code
4. Dense Auto-Placement
.masonry-like {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-auto-flow: dense;
gap: var(--spacing-2);
}
Subgrid: Nested Alignment
Subgrid allows child grids to inherit parent track sizing.
When to Use Subgrid
- Card layouts requiring header/footer alignment across cards
- Form layouts with consistent label/input alignment
- Nested components that must align with parent grid
Syntax
.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; }
Subgrid in One Dimension
.item {
display: grid;
grid-template-columns: subgrid; /* Inherit columns */
grid-template-rows: repeat(3, auto); /* Custom rows */
}
Gap Override in Subgrid
.subgrid-item {
grid-template-columns: subgrid;
grid-template-rows: subgrid;
row-gap: 0; /* Override parent gap */
}
Container Queries: Component-Level Responsiveness
Container queries enable styles based on container size, not viewport.
Basic Setup
/* 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;
}
}
Range Syntax (Modern)
/* Cleaner range queries */
@container (width >= 300px) { }
@container (200px <= width <= 500px) { }
Container Query Units
.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 |
Container Types
/* Width queries only (most common) */
container-type: inline-size;
/* Width AND height queries (requires defined height) */
container-type: size;
Design Token System
No Hardcoded Values Rule
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);
}
Token Categories
| 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 |
Semantic vs Primitive Colors
/* ❌ 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-subtle
Gap over Margin
Replace Margin Utilities with Grid Gap
Never 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>
Conversion Table
| 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 is Still Acceptable
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>
State Management with Data Attributes
Replace Conditional Classes with Data Attributes
/* ❌ 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}
>
CSS for Data Attributes
.dock-panel {
/* Base styles */
&[data-dragging] {
opacity: 0.5;
}
&[data-maximized] {
z-index: var(--z-index-modal-backdrop);
}
}
Benefits
- Clean JSX - No template literal concatenation
- CSS Ownership - Style logic stays in CSS, not JS
- Performance - Attribute selectors are efficient
- Debugging - Data attributes visible in DevTools
- Type Safety - Boolean presence, not string comparison
Inline Style Guidelines
When Inline Styles Are Acceptable
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}%`,
}}
/>
When NOT to Use Inline Styles
/* ❌ 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}>
Remove Unnecessary Style Props
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 />
CSS Architecture Principles
1. Specificity Management
/* ❌ High specificity, hard to override */
.sidebar .nav .nav-item.active a { }
/* ✅ Flat specificity, composable */
.nav-item { }
.nav-item--active { }
2. Naming Convention (BEM-inspired)
/* Block */
.panel { }
/* Element (part of block) */
.panel__header { }
.panel__content { }
.panel__footer { }
/* Modifier (variation) */
.panel--compact { }
.panel--elevated { }
3. Layer Organization (CSS Cascade Layers)
@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 */ }
}
4. Custom Properties Strategy
/* 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);
}
Alignment Reference
Grid Alignment Properties
.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;
}
Alignment Values
| 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 |
Responsive Patterns
Without Media Queries
/* 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));
}
With Container Queries (Preferred)
.widget-container {
container-type: inline-size;
}
@container (width < 300px) {
.widget { flex-direction: column; }
}
@container (width >= 300px) {
.widget { flex-direction: row; }
}
Media Queries (When Necessary)
/* Viewport-dependent layout only */
@media (min-width: 768px) {
.app-layout {
grid-template-areas:
"sidebar header"
"sidebar content";
grid-template-columns: 240px 1fr;
}
}
Performance Guidelines
- Avoid layout thrashing: Batch DOM reads/writes
- Minimize repaints: Use
transformandopacityfor animations - Contain layouts: Use
contain: layoutfor isolated components - Reduce specificity: Flat selectors parse faster
/* Layout containment for performance */
.card {
contain: layout style;
}
Accessibility Considerations
Visual vs DOM Order
/* ⚠️ 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 Indicators
:focus-visible {
outline: 2px solid var(--color-ring);
outline-offset: 2px;
}
Reduced Motion
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
Decision Matrix
Layout Decisions
| 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-* |
Styling Decisions
| 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 |
Modern CSS Features (2024+)
CSS Anchor Positioning
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
CSS @scope
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; }
}
light-dark() Function
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);
}
@property (Typed Custom Properties)
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; }
}
color-mix()
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%);
}
}
OKLCH Color Space
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:
- Consistent perceived brightness across hues
- Predictable color manipulation
- Better for accessibility (contrast calculations)
Browser Support Strategy
/* 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 */
}
}
Border-Radius and Padding Alignment
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.
Code Review Checklist
Before Committing CSS/TSX Changes
- No hardcoded px/rem values - All sizes use
var(--spacing-*),var(--size-*)etc. - No hardcoded colors - All colors use semantic tokens like
var(--color-*)or Tailwind equivalents - No hardcoded z-index - Use
var(--z-index-*)tokens - No margin utilities for spacing - Replace
mt-*,mb-*, etc. with gridgap-* - Grid over Flexbox - Use
gridunless single-axis centering truly requiresflex - No unnecessary inline styles -
style={{ width: "100%", height: "100%" }}→className='w-full h-full' - Data attributes for states - Not conditional class concatenation
- No unnecessary style props - Remove
style?: React.CSSPropertiesif only used for full dimensions - CSS owns styling logic - State-based styles defined in CSS via
&[data-*], not in JSX - Padding >= Border-radius - Ensure padding is at least equal to border-radius to prevent text clipping
Grep Commands for Validation
# 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