Claude Code Plugins

Community-maintained marketplace

Feedback

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

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name css-author
description Modern CSS organization with native @import, @layer cascade control, CSS nesting, design tokens, and element-focused selectors. AUTO-INVOKED when editing .css files.
allowed-tools Read, Write, Edit

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

Category Purpose Examples
Colors Brand, semantic, surface colors --primary, --error
Spacing Consistent gaps and padding --spacing-sm, --spacing-lg
Typography Font sizes, weights, heights --font-size-lg, --line-height-normal
Effects Shadows, transitions, borders --shadow-md, --transition-normal
Layout Widths, 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
Format Use Case Example
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 colors Variations 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

Pattern Example Purpose
--{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:

Avoid Prefer
--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

Layer Priority Purpose
reset Lowest Normalize browser defaults
tokens Low CSS custom properties
layout Medium-Low Body grid, main structure
sections Medium Header, footer, sidebar
components Medium-High Buttons, cards, form elements
pages High Page-specific overrides
responsive Highest Media 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 Type Display Examples
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

Pattern Use 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 @scope With @scope
Selectors leak globally Selectors limited to subtree
Need long descendant chains Short selectors, explicit boundaries
High specificity for isolation Low 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 Case Example
Multi-variant components .card, .card-featured
View transition names .vt-card-1 (when data-* insufficient)
Third-party integration Classes required by libraries

Never use classes for state. Use data-* attributes instead.


Scope Hierarchy

Level Scope Contents
Tokens Entire site Colors, spacing, typography, effects
Layout Body structure Grid areas, view transitions, body rules
Sections Recurring site parts Header, footer, sidebar, navigation
Components Reusable blocks Cards, buttons, forms, tables, tags
Pages Single page types Homepage hero, blog post, contact form

When to Create a New File

Scenario Action
New custom element Create components/_element-name.css
New page type with unique styles Create pages/_page-name.css
New recurring section Create sections/_section-name.css
New design token category Extend _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 Queries Container Queries
Respond to viewport Respond to container
Global breakpoints Component-specific
Same component, same layout everywhere Same 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

Type Queries On Use When
inline-size Width only Most common - responsive layouts
size Width and height Rare - when height matters
normal No size queries Style 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:

Unit Meaning
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 Subgrid With Subgrid
Nested grids are independent Child inherits parent's tracks
Must duplicate track sizes Single source of truth
Alignment breaks across nesting Perfect 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 Case Benefit
Card grids Aligned headers/footers across cards
Form layouts Labels and inputs align vertically
Data tables Column alignment in complex cells
Multi-level navigation Consistent column widths
Article layouts Full-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 Properties Logical Properties
Fixed to screen edges Adapt to text direction
Break in RTL languages Work in any writing mode
Require RTL overrides Automatically 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:

Axis Direction Physical Equivalent
Block Vertical (in LTR/RTL) Top ↔ Bottom
Inline Horizontal (in LTR/RTL) Left ↔ Right

Each axis has two edges:

Edge Block Axis Inline Axis (LTR) Inline Axis (RTL)
Start Top Left Right
End Bottom Right Left

Property Mappings

Margins

Physical Logical
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

Physical Logical
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

Physical Logical
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

Physical Logical
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:

Property Keep 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

Status Meaning Our Approach
Widely available 30+ months in all browsers Use freely
Newly available Recently in all browsers Use freely (our threshold)
Limited availability Not in all browsers Requires @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 Feature Invoke Skill Why
Animations, transitions animation-motion Proper keyframes, scroll-driven effects, reduced-motion
Print styles (@media print) print-styles Print-specific layout, page breaks, hiding nav
Icon styling icons Use <x-icon> component, not inline SVG
Dark/light themes data-attributes State via data-theme, not classes
Responsive images responsive-images Image 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