Claude-skill-registry css-author

Modern CSS organization with native @import, @layer cascade control, CSS nesting, design tokens, and element-focused selectors. AUTO-INVOKED when editing .css files.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/css-author" ~/.claude/skills/majiayu000-claude-skill-registry-css-author && rm -rf "$T"
manifest: skills/data/css-author/SKILL.md
source content

CSS Author Skill

This skill provides patterns for organizing CSS in modern, maintainable ways without build tools. We leverage native CSS features:

@import
for modularization,
@layer
for cascade control, and nesting for readability.

Philosophy

CSS should be:

  1. Native - No preprocessors or build steps required
  2. Modular - Organized by scope and purpose
  3. Predictable - Cascade layers eliminate specificity wars
  4. Semantic - Target elements, not class soup

File Organization Hierarchy

styles/
├── main.css                 # Entry point - imports everything
├── _reset.css               # CSS reset/normalize
├── _tokens.css              # Design tokens (custom properties)
├── _layout.css              # Site-wide layout (grid, body structure)
├── _components.css          # Shared components (buttons, cards)
├── sections/
│   ├── _header.css          # Site header/nav
│   ├── _footer.css          # Site footer
│   └── _sidebar.css         # Sidebar patterns
├── pages/
│   ├── _home.css            # Homepage-specific styles
│   ├── _blog.css            # Blog listing/post styles
│   └── _contact.css         # Contact page styles
└── components/
    ├── _gallery.css         # Gallery grid component
    ├── _tag-list.css        # Tag component styles
    └── _data-table.css      # Table wrapper styles

Naming Convention

  • Underscore prefix (
    _reset.css
    ): Partial files, imported by main.css
  • No prefix (
    main.css
    ): Entry point, linked in HTML
  • Lowercase with hyphens:
    _tag-list.css
    ,
    _data-table.css

The Entry Point (
main.css
)

The main stylesheet declares layers and imports partials:

/* Layer declaration - controls cascade order */
@layer reset, tokens, layout, sections, components, pages, responsive;

/* Reset (lowest priority) */
@import "_reset.css" layer(reset);

/* Design system tokens */
@import "_tokens.css" layer(tokens);

/* Site-wide layout */
@import "_layout.css" layer(layout);

/* Recurring sections */
@import "sections/_header.css" layer(sections);
@import "sections/_footer.css" layer(sections);
@import "sections/_sidebar.css" layer(sections);

/* Shared components */
@import "_components.css" layer(components);
@import "components/_gallery.css" layer(components);
@import "components/_tag-list.css" layer(components);
@import "components/_data-table.css" layer(components);

/* Page-specific styles */
@import "pages/_home.css" layer(pages);
@import "pages/_blog.css" layer(pages);
@import "pages/_contact.css" layer(pages);

/* Responsive overrides (highest priority) */
@layer responsive {
  @media (max-width: 768px) {
    /* Mobile overrides */
  }
}

Design Tokens System

Design tokens are CSS custom properties that provide consistent, themeable values across your design system.

Why Design Tokens?

Design tokens provide:

  1. Consistency - Same values used everywhere
  2. Maintainability - Change once, apply everywhere
  3. Theming - Swap token values for different themes
  4. Documentation - Token names describe purpose

Token Categories

CategoryPurposeExamples
ColorsBrand, semantic, surface colors
--primary
,
--error
SpacingConsistent gaps and padding
--spacing-sm
,
--spacing-lg
TypographyFont sizes, weights, heights
--font-size-lg
,
--line-height-normal
EffectsShadows, transitions, borders
--shadow-md
,
--transition-normal
LayoutWidths, breakpoints
--content-width
,
--sidebar-width

Modern Color Formats

Use OKLCH instead of hex/RGB. OKLCH provides:

  • Perceptually uniform lightness (consistent perceived brightness)
  • Wider color gamut than sRGB
  • Better color interpolation in gradients
  • Easier programmatic color generation
FormatUse CaseExample
oklch()
Primary format for all colors
oklch(55% 0.22 260)
light-dark()
Theme-aware tokens
light-dark(oklch(20% 0 0), oklch(95% 0 0))
color-mix()
Blending, opacity
color-mix(in oklch, var(--primary), transparent 50%)
Relative colorsVariations from base
oklch(from var(--primary) calc(l + 0.2) c h)

OKLCH Syntax

/* oklch(lightness chroma hue) */
--primary: oklch(55% 0.22 260);  /* Blue */
--success: oklch(65% 0.2 145);   /* Green */
--warning: oklch(75% 0.18 85);   /* Orange */
--error: oklch(55% 0.22 25);     /* Red */
  • Lightness: 0% (black) to 100% (white)
  • Chroma: 0 (gray) to ~0.4 (vivid) - varies by hue
  • Hue: 0-360 degrees (0=pink, 90=yellow, 180=cyan, 270=blue)

Relative Colors (Derive Variations)

Generate color variations programmatically from a base color:

:root {
  --primary: oklch(55% 0.22 260);

  /* Lighter: increase lightness */
  --primary-light: oklch(from var(--primary) calc(l + 0.2) c h);

  /* Darker: decrease lightness */
  --primary-dark: oklch(from var(--primary) calc(l - 0.15) c h);

  /* Muted: reduce chroma */
  --primary-muted: oklch(from var(--primary) l calc(c - 0.1) h);

  /* Hover: slightly darker and more saturated */
  --primary-hover: oklch(from var(--primary) calc(l - 0.08) calc(c + 0.02) h);
}

Theme-Aware Colors with
light-dark()

Single declarations for both light and dark themes:

:root {
  color-scheme: light dark;  /* Required for light-dark() */

  /* Single token handles both themes */
  --text: light-dark(oklch(20% 0 0), oklch(95% 0 0));
  --surface: light-dark(oklch(100% 0 0), oklch(15% 0.02 260));
  --border: light-dark(oklch(90% 0.01 260), oklch(30% 0.02 260));
}

Color Mixing for Opacity/Blending

/* Semi-transparent overlays */
--overlay-light: color-mix(in oklch, black, transparent 95%);
--overlay-medium: color-mix(in oklch, black, transparent 90%);

/* Elevated surfaces */
--surface-elevated: color-mix(in oklch, var(--surface), white 5%);

/* Blend two colors */
--accent-blend: color-mix(in oklch, var(--primary), var(--secondary) 30%);

Gradients with Color Space

Specify color space to prevent muddy midtones:

/* Vibrant gradient interpolation */
background: linear-gradient(in oklch, var(--primary), var(--secondary));

/* For hue transitions, use longer path */
background: linear-gradient(in oklch longer hue, oklch(65% 0.25 0), oklch(65% 0.25 360));

Browser Fallbacks

For older browsers, provide hex fallback first:

:root {
  --primary: #2563eb;  /* Fallback for older browsers */
  --primary: oklch(55% 0.22 260);
}

Automatic Contrast with
contrast-color()

The

contrast-color()
function automatically selects black or white text based on background:

/* Button with any background color */
button {
  background: var(--primary);
  color: contrast-color(var(--primary));
}

/* Dynamic accent backgrounds */
[data-accent] {
  background: var(--accent);
  color: contrast-color(var(--accent));
}

Combining with

light-dark()
:

.badge {
  --bg: light-dark(var(--primary-light), var(--primary-dark));
  background: var(--bg);
  color: contrast-color(var(--bg));
}

Limitations:

  • Returns only black (
    #000
    ) or white (
    #fff
    )
  • Uses WCAG 2 algorithm (may not be perceptually optimal for mid-tones)
  • Browser support: Safari Technology Preview only (use as progressive enhancement)
  • Does not guarantee WCAG compliance—verify contrast ratios for critical UI

Best practice: Use

contrast-color()
for dynamic/user-selected colors. For design system colors, manually define text colors to ensure optimal readability.

Complete Token System

/* _tokens.css */
@layer tokens {
  :root {
    /* Enable light-dark() function */
    color-scheme: light dark;

    /* ==================== COLORS (OKLCH) ==================== */

    /* Hue palette - define once, reuse everywhere */
    --hue-primary: 260;   /* Blue */
    --hue-secondary: 250; /* Slate */
    --hue-success: 145;   /* Green */
    --hue-warning: 85;    /* Orange */
    --hue-error: 25;      /* Red */
    --hue-info: 200;      /* Cyan */

    /* Brand colors with relative variations */
    --primary: oklch(55% 0.22 var(--hue-primary));
    --primary-hover: oklch(from var(--primary) calc(l - 0.08) calc(c + 0.02) h);
    --primary-light: oklch(from var(--primary) calc(l + 0.35) calc(c - 0.12) h);
    --secondary: oklch(50% 0.03 var(--hue-secondary));
    --secondary-hover: oklch(from var(--secondary) calc(l - 0.1) c h);

    /* Semantic colors */
    --success: oklch(60% 0.18 var(--hue-success));
    --success-light: oklch(from var(--success) calc(l + 0.32) calc(c - 0.1) h);
    --warning: oklch(75% 0.16 var(--hue-warning));
    --warning-light: oklch(from var(--warning) calc(l + 0.2) calc(c - 0.08) h);
    --error: oklch(55% 0.2 var(--hue-error));
    --error-light: oklch(from var(--error) calc(l + 0.38) calc(c - 0.12) h);
    --info: oklch(55% 0.14 var(--hue-info));
    --info-light: oklch(from var(--info) calc(l + 0.38) calc(c - 0.08) h);

    /* Theme-aware surface colors */
    --background: light-dark(oklch(100% 0 0), oklch(12% 0.02 var(--hue-primary)));
    --background-alt: light-dark(oklch(98% 0.005 var(--hue-primary)), oklch(16% 0.02 var(--hue-primary)));
    --surface: light-dark(oklch(100% 0 0), oklch(16% 0.02 var(--hue-primary)));
    --surface-elevated: light-dark(oklch(100% 0 0), oklch(22% 0.02 var(--hue-primary)));

    /* Theme-aware text colors */
    --text: light-dark(oklch(20% 0.02 var(--hue-primary)), oklch(96% 0.01 var(--hue-primary)));
    --text-muted: light-dark(oklch(45% 0.02 var(--hue-primary)), oklch(65% 0.02 var(--hue-primary)));
    --text-inverted: light-dark(oklch(100% 0 0), oklch(10% 0 0));

    /* Theme-aware border colors */
    --border: light-dark(oklch(90% 0.01 var(--hue-primary)), oklch(28% 0.02 var(--hue-primary)));
    --border-strong: light-dark(oklch(82% 0.01 var(--hue-primary)), oklch(38% 0.02 var(--hue-primary)));

    /* Theme-aware overlays using color-mix */
    --overlay-light: light-dark(
      color-mix(in oklch, black, transparent 95%),
      color-mix(in oklch, white, transparent 95%)
    );
    --overlay-medium: light-dark(
      color-mix(in oklch, black, transparent 90%),
      color-mix(in oklch, white, transparent 90%)
    );
    --overlay-strong: light-dark(
      color-mix(in oklch, black, transparent 80%),
      color-mix(in oklch, white, transparent 80%)
    );

    /* ==================== SPACING ==================== */

    --spacing-xs: 0.25rem;   /* 4px */
    --spacing-sm: 0.5rem;    /* 8px */
    --spacing-md: 1rem;      /* 16px */
    --spacing-lg: 1.5rem;    /* 24px */
    --spacing-xl: 2rem;      /* 32px */
    --spacing-2xl: 3rem;     /* 48px */
    --spacing-3xl: 4rem;     /* 64px */

    /* ==================== TYPOGRAPHY ==================== */

    /* Font families */
    --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    --font-mono: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, monospace;
    --font-serif: Georgia, Cambria, "Times New Roman", Times, serif;

    /* Font sizes */
    --font-size-xs: 0.75rem;   /* 12px */
    --font-size-sm: 0.875rem;  /* 14px */
    --font-size-base: 1rem;    /* 16px */
    --font-size-lg: 1.125rem;  /* 18px */
    --font-size-xl: 1.25rem;   /* 20px */
    --font-size-2xl: 1.5rem;   /* 24px */
    --font-size-3xl: 1.875rem; /* 30px */
    --font-size-4xl: 2.25rem;  /* 36px */

    /* Font weights */
    --font-weight-normal: 400;
    --font-weight-medium: 500;
    --font-weight-semibold: 600;
    --font-weight-bold: 700;

    /* Line heights */
    --line-height-tight: 1.25;
    --line-height-normal: 1.5;
    --line-height-relaxed: 1.625;

    /* ==================== EFFECTS ==================== */

    /* Shadows */
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
    --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
    --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
    --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1);

    /* Transitions */
    --transition-fast: 0.15s ease;
    --transition-normal: 0.3s ease;
    --transition-slow: 0.5s ease;

    /* Border radius */
    --radius-sm: 0.25rem;
    --radius-md: 0.375rem;
    --radius-lg: 0.5rem;
    --radius-xl: 0.75rem;
    --radius-full: 9999px;

    /* ==================== LAYOUT ==================== */

    --content-width: 65ch;
    --content-width-wide: 80rem;
    --sidebar-width: 16rem;

    /* Z-index scale */
    --z-dropdown: 100;
    --z-sticky: 200;
    --z-modal: 300;
    --z-tooltip: 400;
  }
}

Dark Theme Approach

Recommended: Use

light-dark()
in the Complete Token System above. This eliminates the need for duplicate token definitions.

User-Controlled Theme Toggle (Legacy Pattern)

For sites with theme toggle UI that override system preference, use CSS

:has()
to scope token overrides:

/* Force dark mode when user selects dark */
:root:has(#theme-dark:checked) {
  color-scheme: dark;  /* Triggers light-dark() to use dark values */
}

/* Force light mode when user selects light */
:root:has(#theme-light:checked) {
  color-scheme: light;  /* Triggers light-dark() to use light values */
}

/* Auto follows system preference (default behavior) */
:root:has(#theme-auto:checked) {
  color-scheme: light dark;
}

Component-Specific Tokens

:root {
  /* Form tokens */
  --form-border: var(--border);
  --form-focus: var(--primary);
  --form-invalid: var(--error);
  --form-input-padding: var(--spacing-sm) var(--spacing-md);
  --form-input-radius: var(--radius-md);

  /* Button tokens */
  --button-padding: var(--spacing-sm) var(--spacing-lg);
  --button-radius: var(--radius-md);
  --button-primary-bg: var(--primary);
  --button-primary-text: var(--text-inverted);

  /* Card tokens */
  --card-padding: var(--spacing-lg);
  --card-radius: var(--radius-lg);
  --card-shadow: var(--shadow-sm);
  --card-bg: var(--surface);
}

Token Naming Guidelines

PatternExamplePurpose
--{category}
--primary
,
--error
Base tokens (no
-color
suffix)
--{category}-{variant}
--primary-hover
,
--success-light
Token variations
--{element}-{modifier}
--text-muted
,
--border-strong
Semantic element tokens

Use semantic names, not literal values:

AvoidPrefer
--blue
,
--primary-color
--primary
--red
,
--error-color
--error
--16px
--spacing-md
#2563eb
(hex in code)
var(--primary)

CSS Layers (
@layer
)

Why Layers?

Layers provide explicit cascade control regardless of selector specificity:

@layer base, theme, utilities;

@layer utilities {
  .hidden { display: none !important; }
}

@layer base {
  button { display: inline-block; }
}

/* utilities wins over base, even with lower specificity */

Recommended Layer Order

LayerPriorityPurpose
reset
LowestNormalize browser defaults
tokens
LowCSS custom properties
layout
Medium-LowBody grid, main structure
sections
MediumHeader, footer, sidebar
components
Medium-HighButtons, cards, form elements
pages
HighPage-specific overrides
responsive
HighestMedia query adjustments

Layer Benefits

  1. No specificity wars - Later layers always win
  2. Predictable overrides - Page styles override components
  3. Safe imports - Third-party CSS can be isolated
  4. Clear organization - Find styles by layer purpose

Custom Element Display Behavior

Custom elements (hyphenated tags like

<product-card>
) have specific display quirks that require understanding.

Browser Default: Inline

Browsers treat unknown elements as

display: inline
, breaking block-level layouts:

<!-- Renders INLINE by default! -->
<product-card>
  <img src="product.jpg" alt="..." />
  <h3>Product Name</h3>
</product-card>

This causes layout issues because the element doesn't create a block formatting context.

The
:not(:defined)
Solution

The

:not(:defined)
pseudo-class matches custom elements that haven't been registered with
customElements.define()
:

/* In reset layer - catches ALL unregistered custom elements */
:not(:defined) {
  display: block;
}

This is ideal for CSS-only custom elements that will never be registered as Web Components.

Layer Specificity Warning

Critical: Unlayered browser defaults beat layered CSS. Even with

@layer reset { ... }
, browser defaults can override your styles.

/* May NOT work - layer has lower priority than browser default */
@layer reset {
  product-card {
    display: block;
  }
}

/* Solution: :not(:defined) has higher specificity */
:not(:defined) {
  display: block;
}

The
:defined
Pseudo-Class

For registered Web Components, use

:defined
to style after JavaScript loads:

/* Hide until component is defined */
product-card:not(:defined) {
  visibility: hidden;
}

/* Show when registered */
product-card:defined {
  visibility: visible;
}

Block vs Inline Custom Elements

Not all custom elements should be block. Consider the content model:

Element TypeDisplayExamples
Container/Section
block
product-card
,
hero-section
,
card-grid
Badge/Indicator
inline-flex
status-badge
,
tag-item
Icon
inline-flex
x-icon

Elements with

phrasing: true
in
elements.json
are designed to be inline.


List Styling Patterns

Styling lists reliably requires understanding browser defaults and specificity.

Removing Default Bullets

The most reliable pattern for navigation and card lists:

/* In reset layer */
ul, ol {
  list-style: none;
  padding: 0;
  margin: 0;
}

Warning:

list-style-type: none
alone may not work in all contexts. Use the shorthand
list-style: none
for reliability.

Accessibility Note

When you remove bullets from a list, screen readers may not announce it as a list in Safari/VoiceOver. Add

role="list"
to preserve semantics:

<ul role="list">
  <li>Item with no bullet but announced as list</li>
</ul>

Custom Markers with ::marker

For custom bullets, use the

::marker
pseudo-element:

li::marker {
  color: var(--primary);
  content: "→ ";
}

/* For specific lists */
ul[data-style="checkmarks"] li::marker {
  content: "✓ ";
  color: var(--success);
}

Numbered Lists with Custom Styling

ol {
  counter-reset: list-counter;
  list-style: none;
}

ol li {
  counter-increment: list-counter;
}

ol li::before {
  content: counter(list-counter) ". ";
  color: var(--primary);
  font-weight: var(--font-weight-semibold);
}

When to Use Each Pattern

PatternUse Case
list-style: none
Navigation, card grids, tab lists
::marker
Prose lists with custom bullet style
counter()
Numbered steps, ordered lists with custom numbers

CSS Scope (
@scope
)

The

@scope
at-rule limits selector reach to a specific DOM subtree without increasing specificity. While
@layer
controls cascade order,
@scope
controls where selectors can match.

Why
@scope
?

Without @scopeWith @scope
Selectors leak globallySelectors limited to subtree
Need long descendant chainsShort selectors, explicit boundaries
High specificity for isolationLow specificity preserved

Basic Syntax

@scope (product-card) {
  /* These only match inside <product-card> */
  img {
    border-radius: var(--radius-md);
  }

  h3 {
    font-size: var(--font-size-lg);
  }
}

The scoping root (

product-card
) doesn't add to selector specificity—
img
remains
(0,0,1)
.

The
:scope
Pseudo-Class

Reference the scoping root itself:

@scope (blog-card) {
  :scope {
    /* Styles the <blog-card> element */
    display: grid;
    gap: var(--spacing-md);
  }

  h3 {
    /* Styles <h3> inside <blog-card> */
    margin: 0;
  }
}

Donut Scope Pattern

Exclude nested sections with a lower boundary using

to
:

/* Style card chrome, but not user content inside */
@scope (blog-card) to (.card-content) {
  img {
    /* Only matches images in card header/footer, not in content */
    border: 2px solid var(--border);
  }
}

Use cases for donut scope:

  • Style component wrapper but not slotted content
  • Style card header/footer but not body
  • Apply theme to shell but let content inherit differently

@scope
with
@layer

Combine scope and layers for full control:

@layer components {
  @scope (product-card) {
    :scope {
      container-type: inline-size;
      padding: var(--spacing-lg);
    }

    img {
      width: 100%;
      aspect-ratio: 4/3;
      object-fit: cover;
    }

    @container (min-width: 400px) {
      :scope {
        display: grid;
        grid-template-columns: 200px 1fr;
      }
    }
  }
}

@scope
vs Element Selectors

Both work for our custom element approach:

/* Element selector (our typical pattern) */
product-card {
  display: grid;
}

product-card img {
  border-radius: var(--radius-md);
}

/* @scope equivalent - cleaner for many child rules */
@scope (product-card) {
  :scope {
    display: grid;
  }

  img {
    border-radius: var(--radius-md);
  }

  h3 { }
  p { }
  footer { }
}

When to use

@scope
:

  • Component has many child element rules
  • Need donut scope to exclude nested content
  • Want to group all component styles in one block

When element selectors suffice:

  • Simple components with few rules
  • Already using nesting effectively

Prelude-less Scope (Inline Styles)

In component HTML, scope without a selector:

<product-card>
  <style>
    @scope {
      :scope { display: grid; }
      img { border-radius: var(--radius-md); }
    }
  </style>
  <img src="..." alt="..." />
  <h3>Product Name</h3>
</product-card>

The scope automatically targets the parent element.

Important Limitation

@scope
limits selector reach, not inheritance. Inherited properties like
color
still cascade into excluded donut holes:

@scope (.card) to (.content) {
  :scope {
    color: blue;  /* .content still inherits blue! */
  }
}

To prevent inheritance, reset properties explicitly on the excluded element.

Browser Support

  • Chrome 118+, Edge 118+, Safari 17.4+, Firefox 146+
  • Wide support (90%+) - safe to use without fallbacks

Native CSS Nesting

Modern browsers support CSS nesting, reducing repetition:

/* Without nesting */
nav { }
nav ul { }
nav a { }
nav a:hover { }

/* With nesting */
nav {
  & ul {
    display: flex;
    gap: var(--spacing-lg);
  }

  & a {
    padding: var(--spacing-sm) var(--spacing-md);

    &:hover {
      background: var(--overlay-light);
    }

    &[aria-current="page"] {
      background: var(--overlay-strong);
    }
  }
}

Nesting Rules

  1. Use
    &
    for clarity
    - Always prefix nested selectors with
    &
  2. Limit depth - No more than 3-4 levels deep
  3. Keep related styles together - Element and its states
  4. Avoid over-nesting - If selectors get complex, flatten

Nesting with Media Queries

Media queries can be nested inside selectors:

header {
  padding: var(--spacing-lg);

  @media (max-width: 768px) {
    padding: var(--spacing-md);
  }
}

Element-Focused CSS (Classless)

Target Semantic HTML

Instead of inventing classes, style semantic elements:

/* Avoid */
.header-nav { }
.nav-list { }
.nav-link { }

/* Prefer */
header nav { }
header nav ul { }
header nav a { }

Custom Elements as Styling Hooks

Custom elements provide semantic styling targets without classes:

/* Instead of .form-group { } */
form-field { }

/* Instead of .product-card { } */
product-card { }

/* Instead of .table-wrapper { } */
table-wrapper { }

When Classes Are Appropriate

Use classes sparingly for:

Use CaseExample
Multi-variant components
.card
,
.card-featured
View transition names
.vt-card-1
(when data-* insufficient)
Third-party integrationClasses required by libraries

Never use classes for state. Use

data-*
attributes instead.


Scope Hierarchy

LevelScopeContents
TokensEntire siteColors, spacing, typography, effects
LayoutBody structureGrid areas, view transitions, body rules
SectionsRecurring site partsHeader, footer, sidebar, navigation
ComponentsReusable blocksCards, buttons, forms, tables, tags
PagesSingle page typesHomepage hero, blog post, contact form

When to Create a New File

ScenarioAction
New custom elementCreate
components/_element-name.css
New page type with unique stylesCreate
pages/_page-name.css
New recurring sectionCreate
sections/_section-name.css
New design token categoryExtend
_tokens.css

Adding a New CSS File

1. Create the Partial

/* components/_gallery.css */
@layer components {
  gallery-grid {
    display: grid;
    gap: var(--spacing-md);

    &[data-columns="2"] { grid-template-columns: repeat(2, 1fr); }
    &[data-columns="3"] { grid-template-columns: repeat(3, 1fr); }
    &[data-columns="4"] { grid-template-columns: repeat(4, 1fr); }
  }
}

2. Add Import to main.css

/* In main.css, add to appropriate section */
@import "components/_gallery.css" layer(components);

3. File Template

Every partial should follow this structure:

/* components/_example.css */
@layer components {
  /* Element styles */
  example-element {
    /* Base styles */

    /* State variants via data attributes */
    &[data-state="active"] { }

    /* Nested elements */
    & .inner { }

    /* Responsive adjustments */
    @media (max-width: 768px) { }
  }
}

CSS Import Performance

Browser Behavior

Modern browsers handle

@import
efficiently:

  • Parallel fetching when imports are at the start
  • Caching of individual files
  • No render-blocking beyond the cascade order

Best Practices

  1. All imports at the top - Before any other CSS
  2. Layer declaration first -
    @layer
    before
    @import
  3. Use HTTP/2 - Multiplexing handles multiple files well
  4. Consider concatenation for production if needed

When to Consolidate

For very high-traffic sites, you may want to concatenate CSS:

# Simple concatenation for production
cat styles/_reset.css styles/_tokens.css styles/_layout.css > styles/bundle.css

But for most projects, native imports work well.


Responsive Design Pattern

Mobile-First vs Desktop-First

We use desktop-first with

max-width
queries, grouped in the
responsive
layer:

@layer responsive {
  @media (max-width: 1024px) {
    /* Tablet adjustments */
  }

  @media (max-width: 768px) {
    /* Mobile adjustments */
  }

  @media (max-width: 480px) {
    /* Small mobile adjustments */
  }
}

Breakpoint Tokens

Define breakpoints as documentation (CSS can't use variables in media queries):

/* _tokens.css */
:root {
  /* Breakpoints (for reference - use literal values in @media) */
  /* --breakpoint-xl: 1280px; */
  /* --breakpoint-lg: 1024px; */
  /* --breakpoint-md: 768px; */
  /* --breakpoint-sm: 480px; */
}

Container Queries (
@container
)

Container queries enable component-scoped responsive design. Unlike media queries (which respond to viewport size), container queries respond to the size of a parent container.

Why Container Queries?

Media QueriesContainer Queries
Respond to viewportRespond to container
Global breakpointsComponent-specific
Same component, same layout everywhereSame component adapts to context

Use case: A card component that displays horizontally in a wide sidebar but stacks vertically in a narrow sidebar—without knowing where it's placed.

Defining a Container

Use

container-type
to establish a containment context:

/* Any element can be a container */
sidebar-panel {
  container-type: inline-size;  /* Width-based queries */
  container-name: sidebar;      /* Optional: name for targeting */
}

/* Shorthand */
main-content {
  container: content / inline-size;  /* name / type */
}

Container Types

TypeQueries OnUse When
inline-size
Width onlyMost common - responsive layouts
size
Width and heightRare - when height matters
normal
No size queriesStyle queries only

Recommendation: Use

inline-size
for 99% of cases.

Writing Container Queries

/* Query any ancestor container */
@container (min-width: 400px) {
  blog-card {
    display: grid;
    grid-template-columns: 200px 1fr;
  }
}

/* Query a specific named container */
@container sidebar (max-width: 300px) {
  blog-card {
    flex-direction: column;
  }
}

Container Query Units

Container-relative units for truly fluid components:

UnitMeaning
cqw
1% of container width
cqh
1% of container height
cqi
1% of container inline size
cqb
1% of container block size
cqmin
Smaller of
cqi
or
cqb
cqmax
Larger of
cqi
or
cqb

Fluid Typography with Container Units

blog-card h3 {
  /* Font scales with container width, respects user zoom */
  font-size: clamp(1rem, 0.875rem + 0.5cqi, 1.5rem);
}

Rhythm-Aligned Spacing

Combine container units with

lh
(line-height) units for vertical rhythm:

blog-card {
  /* Gap scales with container but rounds to quarter-line increments */
  --gap: round(up, 2cqi, 0.25lh);
  gap: var(--gap);
}

The

round()
function ensures spacing aligns to the typographic grid.

Important Limitation

Container units cannot measure the element they're applied to. This would create a circular dependency. Use nested elements or wrapper patterns:

/* WRONG - card can't size based on its own container */
product-card {
  container-type: inline-size;
  padding: 2cqi;  /* Measures parent, not self! */
}

/* CORRECT - children measure the card container */
product-card {
  container-type: inline-size;
}

product-card > * {
  padding: 2cqi;  /* Now measures product-card */
}

Container Queries with Layers

Container queries integrate naturally with the layer system:

@layer components {
  /* Define containers at the component wrapper level */
  card-container {
    container-type: inline-size;
  }

  /* Base card styles */
  blog-card {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-md);
  }

  /* Container-responsive layout */
  @container (min-width: 500px) {
    blog-card {
      flex-direction: row;
    }

    blog-card img {
      width: 40%;
      flex-shrink: 0;
    }
  }
}

Pattern: Self-Contained Responsive Components

Make components that adapt without external configuration:

/* components/_product-card.css */
@layer components {
  product-card {
    /* The card IS its own container */
    container-type: inline-size;

    display: grid;
    gap: var(--spacing-md);
    padding: var(--spacing-lg);
  }

  /* Compact layout (narrow) */
  @container (max-width: 299px) {
    product-card {
      text-align: center;

      & img {
        margin-inline: auto;
        max-width: 150px;
      }
    }
  }

  /* Standard layout (medium) */
  @container (min-width: 300px) and (max-width: 499px) {
    product-card {
      grid-template-columns: 1fr;
    }
  }

  /* Wide layout (large) */
  @container (min-width: 500px) {
    product-card {
      grid-template-columns: 200px 1fr;
      grid-template-rows: auto 1fr auto;

      & img {
        grid-row: 1 / -1;
      }
    }
  }
}

Container Queries vs Media Queries

Use both—they serve different purposes:

@layer components {
  blog-card {
    container-type: inline-size;
  }

  /* Container query: responds to where card is placed */
  @container (min-width: 400px) {
    blog-card {
      grid-template-columns: 150px 1fr;
    }
  }
}

@layer responsive {
  /* Media query: global layout changes */
  @media (max-width: 768px) {
    .card-grid {
      grid-template-columns: 1fr;  /* Stack cards on mobile */
    }
  }
}

Nesting Container Queries

Container queries can be nested inside element selectors:

sidebar-panel {
  container-type: inline-size;

  & blog-card {
    padding: var(--spacing-md);

    @container (min-width: 350px) {
      padding: var(--spacing-lg);
      display: grid;
      grid-template-columns: 100px 1fr;
    }
  }
}

Container Query Checklist

When implementing container queries:

  • Set
    container-type: inline-size
    on the containing element
  • Use
    container-name
    when multiple containers need targeting
  • Prefer
    min-width
    for progressive enhancement
  • Use container units (
    cqi
    ,
    cqw
    ) for fluid typography/spacing
  • Apply container units to children, not the container element itself
  • Use
    round()
    with
    lh
    units for rhythm-aligned spacing
  • Keep container queries in the same layer as component styles
  • Test components in various container widths

CSS Subgrid

Subgrid allows nested elements to participate in their parent's grid, enabling alignment across nested structures without duplicating track definitions.

Why Subgrid?

Without SubgridWith Subgrid
Nested grids are independentChild inherits parent's tracks
Must duplicate track sizesSingle source of truth
Alignment breaks across nestingPerfect alignment across levels

Basic Subgrid Pattern

/* Parent grid */
.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--spacing-lg);
}

/* Card spans parent columns, subgrids rows */
.card {
  display: grid;
  grid-template-rows: auto 1fr auto;  /* header, content, footer */
}

/* With subgrid: all cards align their internal rows */
.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: auto 1fr auto;  /* Define rows at parent level */
  gap: var(--spacing-lg);
}

.card {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;  /* Inherit parent's row tracks */
}

Subgrid for Form Alignment

Align labels and inputs across form fields:

form {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--spacing-md);
}

form-field {
  display: grid;
  grid-column: span 2;
  grid-template-columns: subgrid;
}

form-field label {
  grid-column: 1;
}

form-field input {
  grid-column: 2;
}

Subgrid for Card Components

Cards with aligned headers, content, and footers:

/* Define consistent structure at grid level */
product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  grid-auto-rows: auto 1fr auto;  /* image, details, actions */
  gap: var(--spacing-lg);
}

product-card {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
  gap: var(--spacing-md);
}

product-card img { grid-row: 1; }
product-card .details { grid-row: 2; }
product-card .actions { grid-row: 3; }

Subgrid in Both Directions

Inherit both column and row tracks:

.parent {
  display: grid;
  grid-template-columns: 1fr 2fr 1fr;
  grid-template-rows: auto 1fr auto;
}

.child {
  grid-column: 1 / -1;
  grid-row: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  grid-template-rows: subgrid;
}

Named Lines with Subgrid

Named lines pass through to subgrid:

.layout {
  display: grid;
  grid-template-columns:
    [full-start] 1fr
    [content-start] minmax(0, 60ch)
    [content-end] 1fr
    [full-end];
}

.content {
  grid-column: full-start / full-end;
  display: grid;
  grid-template-columns: subgrid;
}

/* Child can use parent's named lines */
.content h1 {
  grid-column: content-start / content-end;
}

.content .full-bleed {
  grid-column: full-start / full-end;
}

When to Use Subgrid

Use CaseBenefit
Card gridsAligned headers/footers across cards
Form layoutsLabels and inputs align vertically
Data tablesColumn alignment in complex cells
Multi-level navigationConsistent column widths
Article layoutsFull-bleed elements with named lines

Browser Support Note

Subgrid has good modern browser support (90%+). For older browsers, the fallback is a regular nested grid which may not align perfectly but remains functional.


CSS Logical Properties

Logical properties replace physical direction properties (left, right, top, bottom) with flow-relative alternatives. This enables layouts that automatically adapt to different writing modes and text directions.

Why Logical Properties?

Physical PropertiesLogical Properties
Fixed to screen edgesAdapt to text direction
Break in RTL languagesWork in any writing mode
Require RTL overridesAutomatically flip
margin-left: 1rem
margin-inline-start: 1rem

Benefits:

  • Internationalization - Layouts work in Arabic, Hebrew, and other RTL languages
  • Future-proof - Vertical writing modes (CJK) work automatically
  • Consistency - One codebase for all languages
  • Semantic - Properties describe intent, not position

The Logical Model

CSS logical properties use two axes:

AxisDirectionPhysical Equivalent
BlockVertical (in LTR/RTL)Top ↔ Bottom
InlineHorizontal (in LTR/RTL)Left ↔ Right

Each axis has two edges:

EdgeBlock AxisInline Axis (LTR)Inline Axis (RTL)
StartTopLeftRight
EndBottomRightLeft

Property Mappings

Margins

PhysicalLogical
margin-top
margin-block-start
margin-bottom
margin-block-end
margin-left
margin-inline-start
margin-right
margin-inline-end

Shorthand properties:

/* Two values: start and end */
margin-block: 1rem 2rem;   /* top: 1rem, bottom: 2rem */
margin-inline: 1rem 2rem;  /* left: 1rem (LTR), right: 1rem (RTL) */

/* Single value: both start and end */
margin-block: 1rem;        /* top and bottom */
margin-inline: 1rem;       /* left and right */

Padding

Same pattern as margins:

padding-block: var(--spacing-lg);
padding-inline: var(--spacing-md);

/* Individual sides */
padding-block-start: var(--spacing-lg);
padding-inline-end: var(--spacing-sm);

Sizing

PhysicalLogical
width
inline-size
height
block-size
min-width
min-inline-size
max-height
max-block-size
blog-card {
  inline-size: 100%;
  max-inline-size: 40rem;
  min-block-size: 200px;
}

Positioning

PhysicalLogical
top
inset-block-start
bottom
inset-block-end
left
inset-inline-start
right
inset-inline-end

Shorthand:

/* All four sides */
inset: 0;  /* Same as top: 0; right: 0; bottom: 0; left: 0; */

/* Block and inline axes */
inset-block: 0;   /* top and bottom */
inset-inline: 0;  /* left and right */

Borders

/* Border on one logical side */
border-inline-start: 3px solid var(--primary);

/* Border radius */
border-start-start-radius: var(--radius-lg);  /* top-left in LTR */
border-end-start-radius: var(--radius-lg);    /* bottom-left in LTR */

Text Alignment

PhysicalLogical
text-align: left
text-align: start
text-align: right
text-align: end

Common Patterns

Centering with Logical Properties

/* Center horizontally (works in RTL) */
blog-card {
  margin-inline: auto;
  max-inline-size: 40rem;
}

Icon + Text Spacing

/* Space between icon and text, flips in RTL */
button svg {
  margin-inline-end: var(--spacing-sm);
}

Sidebar Layout

/* Sidebar on the start edge (left in LTR, right in RTL) */
main-layout {
  display: grid;
  grid-template-columns: 250px 1fr;
}

sidebar-panel {
  border-inline-end: 1px solid var(--border);
  padding-inline-end: var(--spacing-lg);
}

Card with Accent Border

/* Accent border on start edge */
blog-card[data-featured] {
  border-inline-start: 4px solid var(--primary);
  padding-inline-start: var(--spacing-lg);
}

Migration Guide

When converting existing CSS:

/* Before */
.card {
  margin-left: 1rem;
  margin-right: 1rem;
  padding-top: 2rem;
  padding-bottom: 1rem;
  border-left: 3px solid blue;
  text-align: left;
}

/* After */
.card {
  margin-inline: 1rem;
  padding-block: 2rem 1rem;
  border-inline-start: 3px solid blue;
  text-align: start;
}

When to Keep Physical Properties

Some properties should remain physical:

PropertyKeep Physical When
top
,
left
, etc.
Fixed position relative to viewport
transform
Animations that shouldn't flip
box-shadow
Light source should stay consistent
background-position
Image positioning shouldn't flip
/* Physical: shadow direction stays consistent */
blog-card {
  box-shadow: 2px 2px 8px oklch(0% 0 0 / 0.15);
}

/* Logical: border flips with text direction */
blog-card {
  border-inline-start: 3px solid var(--primary);
}

Integration with Design Tokens

Define spacing tokens and use them with logical properties:

/* _tokens.css */
:root {
  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 2rem;
}

/* Component using logical properties with tokens */
article {
  padding-block: var(--spacing-xl);
  padding-inline: var(--spacing-lg);
  margin-block-end: var(--spacing-lg);
}

Browser Support

Logical properties have excellent browser support (95%+). For older browsers:

/* Fallback pattern (only if supporting very old browsers) */
blog-card {
  margin-left: 1rem;  /* Fallback */
  margin-inline-start: 1rem;  /* Modern browsers */
}

Example: Complete Component File

/* components/_blog-card.css */
@layer components {
  blog-card {
    display: grid;
    gap: var(--spacing-md);
    padding: var(--spacing-lg);
    background: var(--surface);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    transition: box-shadow var(--transition-normal);

    /* Hover effect */
    &:hover {
      box-shadow: var(--shadow-md);
    }

    /* Featured variant */
    &[data-featured] {
      border-inline-start: 4px solid var(--primary);
    }

    /* Child elements */
    & h3 {
      margin: 0;
      font-size: var(--font-size-lg);
    }

    & time {
      color: var(--text-muted);
      font-size: var(--font-size-sm);
    }

    & p {
      margin: 0;
      line-height: var(--line-height-relaxed);
    }

    /* Responsive */
    @media (max-width: 768px) {
      padding: var(--spacing-md);
    }
  }
}

CSS Baseline

Baseline defines which CSS features are available across all major browsers. Our linter warns when using features outside Baseline Newly available status.

Baseline Tiers

StatusMeaningOur Approach
Widely available30+ months in all browsersUse freely
Newly availableRecently in all browsersUse freely (our threshold)
Limited availabilityNot in all browsersRequires
@supports

Progressive Enhancement for Non-Baseline

Features outside Baseline must be wrapped in

@supports
:

/* Base: Baseline-safe fallback */
p {
  word-break: break-word;
}

/* Enhancement: non-Baseline feature */
@supports (text-wrap: pretty) {
  p {
    text-wrap: pretty;
  }
}

The linter allows non-Baseline CSS inside

@supports
blocks.

Common Non-Baseline Features

Some features we document may not yet be Baseline. Always check and use

@supports
:

/* contrast-color() - Safari Tech Preview only */
@supports (color: contrast-color(red)) {
  .dynamic-bg {
    color: contrast-color(var(--bg));
  }
}

/* text-wrap: pretty - recently Baseline */
@supports (text-wrap: pretty) {
  article p {
    text-wrap: pretty;
  }
}

Checking Baseline Status

  • web.dev/baseline - Feature status lookup
  • caniuse.com - Detailed browser support
  • Run
    npm run lint:css
    - Linter warns on non-Baseline features

Checklist for CSS Architecture

When setting up or reviewing CSS:

Structure

  • Layer declaration at top of main.css
  • All imports use
    layer()
    syntax
  • Files organized by scope (tokens, layout, sections, components, pages)
  • No classes used for state (use
    data-*
    attributes)
  • Custom elements used as styling hooks
  • Nesting limited to 3-4 levels
  • Responsive styles in
    responsive
    layer
  • Design tokens in
    _tokens.css
  • Consider
    @scope
    for components with many child rules or donut patterns

Colors

  • Colors defined in OKLCH format, not hex or RGB
  • color-scheme: light dark
    declared in
    :root
  • Theme-aware tokens use
    light-dark()
    function
  • Color variations use relative colors (not separate tokens)
  • Gradients specify color space:
    linear-gradient(in oklch, ...)
  • Hex fallback provided before OKLCH for older browsers (if needed)

Layout

  • Container queries used for component-scoped responsiveness
  • Components define
    container-type
    when children need to adapt
  • Logical properties used for margins, padding, and borders
  • margin-inline
    /
    padding-block
    instead of physical directions
  • text-align: start
    instead of
    text-align: left
  • Physical properties only where semantically appropriate (shadows, transforms)

Baseline

  • Non-Baseline features wrapped in
    @supports
  • Baseline-safe fallback provided before enhancement
  • npm run lint:css
    passes without baseline warnings

Skills to Consider Before Writing

When authoring CSS, consider invoking these related skills:

CSS FeatureInvoke SkillWhy
Animations, transitionsanimation-motionProper keyframes, scroll-driven effects, reduced-motion
Print styles (@media print)print-stylesPrint-specific layout, page breaks, hiding nav
Icon stylingiconsUse
<x-icon>
component, not inline SVG
Dark/light themesdata-attributesState via
data-theme
, not classes
Responsive imagesresponsive-imagesImage sizing, aspect ratios, art direction

When Styling Components with Icons

When styling buttons, toggles, or UI elements that need icons, ensure the HTML uses

<x-icon>
:

/* Styling icons is simple when using x-icon */
button x-icon {
  color: currentColor;
}

button:hover x-icon {
  color: var(--primary);
}

See the icons skill before adding any visual indicators to HTML.

Related Skills

  • layout-grid - Fluid grid systems, responsive columns, resolution-independent layouts
  • typography - Type scale, hierarchy, rhythm, text-wrap, font pairing
  • animation-motion - CSS animations, transitions, and scroll-driven effects
  • print-styles - Write print-friendly CSS using @media print
  • icons - Lucide icon library with
    <x-icon>
    Web Component
  • data-attributes - Using data-* attributes for state and variants
  • xhtml-author - Write valid XHTML-strict HTML5 markup
  • responsive-images - Modern responsive image techniques
  • progressive-enhancement - HTML-first development with CSS-only interactivity