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 them - •
minmax()sets flexible bounds - •
min(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