css-architecture

Installation
SKILL.md

Rails CSS Architecture Expert

Write maintainable, scalable CSS for Rails applications using modern CSS features — custom properties, @layer, light-dark(), and semantic design tokens. No framework dependency.

Philosophy

Core Principles:

  1. Components own their styles — One CSS file per component, never style components from page stylesheets
  2. Design tokens over magic numbers — All values come from _global.css custom properties
  3. Layers control the cascade — Use @layer instead of specificity hacks or !important
  4. Dark mode is freecolor-scheme + light-dark() gives you dark mode with zero extra work
  5. Utilities are seasoning, not the meal — Use sparingly for one-off adjustments, not as primary styling

Architecture Pyramid:

      /\       Utilities (few — spacing/text tweaks)
     /  \
    /____\     Components (many — reusable UI pieces)
   /      \
  /________\   Base (element defaults, resets)
 /          \
/____________\ Tokens (_global.css — design system foundation)

When To Use This Skill

  • Creating or reorganizing CSS file structure in a Rails app
  • Writing new component stylesheets
  • Implementing dark mode support
  • Defining or extending design tokens
  • Debugging cascade/specificity issues
  • Converting from Tailwind or other frameworks to vanilla CSS
  • Setting up CSS layers
  • Creating utility classes
  • Integrating third-party CSS libraries

Instructions

Step 1: Understand the File Structure

Every Rails app using this architecture has this structure:

app/assets/stylesheets/
├── _global.css              # Design tokens: colors, spacing, typography, radii
├── application.css          # Entry point — may just import everything
├── reset.css                # CSS reset / normalization
├── base.css                 # Element defaults (no classes)
├── utilities.css            # Single-purpose utility classes
└── components/
    ├── app-layout.css       # Main layout shell
    ├── forms.css            # Form elements + buttons
    ├── cards.css            # Card component
    ├── badges.css           # Badge variants
    ├── alerts.css           # Alerts/flash messages
    ├── tables.css           # Table styles
    └── [feature].css        # Feature-specific components

ALWAYS check the existing structure first:

# See what exists
find app/assets/stylesheets -name "*.css" | sort

# Check for existing tokens
cat app/assets/stylesheets/_global.css

# Check layer declarations
rg "@layer" app/assets/stylesheets/

# Check for existing components
ls app/assets/stylesheets/components/

Match existing project conventions — consistency beats "ideal" patterns.

Step 2: Set Up CSS Layers

Declare layers once at the top of _global.css (or application.css):

/* _global.css — FIRST LINE */
@layer reset, base, components, utilities;

Layer priority (lowest → highest):

  1. reset — Browser normalization
  2. base — Element defaults (h1, a, input)
  3. components — UI components (.card, .btn, .badge)
  4. utilities — Override helpers (.mt-4, .hidden) — always wins

Every CSS rule goes inside its layer:

/* base.css */
@layer base {
  body { font-family: var(--font-sans); color: var(--color-ink); }
  a { color: var(--color-link); }
}

/* components/cards.css */
@layer components {
  .card { background: var(--color-surface); border: 1px solid var(--color-border); }
}

/* utilities.css */
@layer utilities {
  .mt-4 { margin-top: var(--space-4); }
}

Why layers matter: Without @layer, you fight specificity with nesting, !important, or load order hacks. Layers eliminate all of that. A utility in the utilities layer beats any component rule regardless of specificity.

Step 3: Define Design Tokens

All design values live in _global.css as custom properties on :root:

:root {
  /* Enable dark mode detection */
  color-scheme: light dark;

  /* === Raw Palette (OKLCH for perceptual uniformity) === */
  --color-zinc-50:  oklch(0.985 0 0);
  --color-zinc-100: oklch(0.967 0.001 286.375);
  --color-zinc-200: oklch(0.92 0.004 286.32);
  --color-zinc-400: oklch(0.707 0.022 261);
  --color-zinc-500: oklch(0.552 0.016 285.938);
  --color-zinc-700: oklch(0.37 0.013 285.805);
  --color-zinc-800: oklch(0.274 0.006 286.033);
  --color-zinc-900: oklch(0.21 0.006 285.885);
  --color-zinc-950: oklch(0.141 0.005 285.823);

  /* === Semantic Color Tokens === */
  /* Surfaces */
  --color-canvas: light-dark(var(--color-zinc-50), var(--color-zinc-900));
  --color-surface: light-dark(white, var(--color-zinc-800));
  --color-surface-raised: light-dark(white, var(--color-zinc-700));
  --color-surface-muted: light-dark(var(--color-zinc-100), var(--color-zinc-800));

  /* Text */
  --color-ink: light-dark(var(--color-zinc-900), var(--color-zinc-50));
  --color-ink-muted: light-dark(var(--color-zinc-500), var(--color-zinc-400));
  --color-ink-subtle: light-dark(var(--color-zinc-400), var(--color-zinc-500));

  /* Borders */
  --color-border: light-dark(var(--color-zinc-200), var(--color-zinc-700));
  --color-border-muted: light-dark(var(--color-zinc-100), var(--color-zinc-800));
  --color-border-strong: light-dark(var(--color-zinc-400), var(--color-zinc-600));

  /* Interactive */
  --color-primary: light-dark(var(--color-blue-600), var(--color-blue-500));
  --color-primary-hover: light-dark(var(--color-blue-700), var(--color-blue-400));
  --color-link: light-dark(var(--color-blue-600), var(--color-blue-400));

  /* State */
  --color-positive: light-dark(var(--color-green-600), var(--color-green-500));
  --color-positive-canvas: light-dark(var(--color-green-50), var(--color-green-950));
  --color-negative: light-dark(var(--color-red-600), var(--color-red-500));
  --color-negative-canvas: light-dark(var(--color-red-50), var(--color-red-950));

  /* === Spacing Scale === */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-12: 3rem;

  /* === Typography === */
  --font-sans: system-ui, -apple-system, sans-serif;
  --font-mono: ui-monospace, "SF Mono", monospace;
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.25rem;
  --text-2xl: 1.5rem;
  --font-medium: 500;
  --font-semibold: 600;
  --font-bold: 700;

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

  /* === Transitions === */
  --duration-fast: 150ms;
  --duration-normal: 250ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}

Rule: Components NEVER use raw values. Always reference tokens.

/* GOOD */
.card { padding: var(--space-6); background: var(--color-surface); }

/* BAD — hardcoded values */
.card { padding: 1.5rem; background: white; }

Step 4: Write Component Styles

Each component gets its own file in components/. Components use tokens only.

Component structure:

/* components/cards.css */
@layer components {
  /* Base */
  .card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--space-6);
  }

  /* Structural subcomponents */
  .card-header { padding-bottom: var(--space-4); border-bottom: 1px solid var(--color-border-muted); }
  .card-body { padding-top: var(--space-4); }
  .card-footer { padding-top: var(--space-4); border-top: 1px solid var(--color-border-muted); }

  /* Variants */
  .card-sm { padding: var(--space-4); }
  .card-lg { padding: var(--space-8); }

  /* States */
  .card-interactive { transition: border-color var(--duration-fast) var(--ease-out); }
  .card-interactive:hover { border-color: var(--color-border-strong); }
}

Naming conventions:

  • Base: .card, .btn, .badge
  • Subcomponents: .card-header, .card-body
  • Variants: .card-sm, .btn-primary
  • States: .is-loading, .is-active

When to create a new component file:

  • Pattern used 3+ times
  • Has related sub-classes (.foo, .foo-header, .foo-item)
  • Has variants (.foo-primary, .foo-small)

Keep it in a page stylesheet when:

  • Truly page-specific (used once)
  • Simple layout (grid, flex container)

Step 5: Dark Mode with light-dark()

Dark mode is automatic when you follow the token system. Here's how:

  1. Set color-scheme: light dark on :root — browsers respect OS preference
  2. Define semantic tokens with light-dark() — first value = light, second = dark
  3. Components reference semantic tokens — they adapt automatically
:root {
  color-scheme: light dark;
  --color-surface: light-dark(white, var(--color-zinc-800));
  --color-ink: light-dark(var(--color-zinc-900), var(--color-zinc-50));
}

/* This component supports dark mode with ZERO extra work */
.card {
  background: var(--color-surface);
  color: var(--color-ink);
}

Optional manual toggle:

html[data-theme="light"] { color-scheme: light; }
html[data-theme="dark"]  { color-scheme: dark; }
function toggleTheme() {
  const current = document.documentElement.dataset.theme;
  const next = current === 'dark' ? 'light' : 'dark';
  document.documentElement.dataset.theme = next;
  localStorage.setItem('theme', next);
}

Flash prevention — put this in <head>:

<script>
  const theme = localStorage.getItem('theme');
  if (theme) document.documentElement.dataset.theme = theme;
</script>

Dark mode rules:

  • Shadows don't work in dark mode — prefer borders
  • Use color-mix() for subtle tinted backgrounds
  • State colors need both a foreground and canvas variant
  • Never use raw white/black — always use semantic tokens

Step 6: Write Utility Classes

Utilities go in utilities.css inside @layer utilities. They are single-purpose and immutable:

@layer utilities {
  /* Spacing */
  .mt-2 { margin-top: var(--space-2); }
  .mt-4 { margin-top: var(--space-4); }
  .mt-6 { margin-top: var(--space-6); }
  .mb-4 { margin-bottom: var(--space-4); }
  .p-4 { padding: var(--space-4); }
  .gap-4 { gap: var(--space-4); }

  /* Text */
  .text-sm { font-size: var(--text-sm); }
  .text-muted { color: var(--color-ink-muted); }
  .font-medium { font-weight: var(--font-medium); }

  /* Layout */
  .flex { display: flex; }
  .grid { display: grid; }
  .hidden { display: none; }
  .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
}

Use utilities for one-off adjustments:

<%# Good — utility for spacing adjustment %>
<div class="card mt-6">

<%# Bad — utilities replacing component styles %>
<div class="p-6 bg-white border rounded-lg shadow">

Step 7: Page Stylesheets Are Thin

Page-specific styles should be minimal — just layout:

/* pages/dashboard.css */
@layer components {
  .dashboard-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: var(--space-6);
  }
}

Pages compose existing pieces:

<div class="dashboard-grid">
  <div class="card">...</div>
  <div class="card">...</div>
  <div class="card mt-4">...</div>  <%# Component + utility %>
</div>

Never style components from page stylesheets:

/* BAD — page overriding component */
.dashboard .card { padding: 2rem; }

/* GOOD — create a variant in the component file */
.card-lg { padding: var(--space-8); }

Step 8: Integrate Third-Party CSS

When integrating external libraries, map their variables to your tokens:

:root {
  --lexxy-color-ink: var(--color-ink);
  --lexxy-color-canvas: var(--color-surface);
  --lexxy-color-selected: light-dark(oklch(0.92 0.026 254), oklch(0.3 0.05 254));
}

Load order matters — library CSS BEFORE your overrides:

<%= stylesheet_link_tag "lexxy" %>
<%= stylesheet_link_tag :app %>

Anti-Patterns

Don't Do Instead
Hardcode colors (white, #333) Use semantic tokens (var(--color-surface))
!important Use @layer for cascade control
Deep nesting (.page .section .card .title) Flat, specific classes (.card-title)
Generic names (.container, .title) Prefixed names (.page-container, .card-title)
Style components from page CSS Create component variants
Inline styles in ERB Use utility classes or component classes
box-shadow for dark mode elevation Use borders
Magic numbers (padding: 23px) Use spacing tokens (var(--space-6))
Media queries for dark mode Use light-dark() with color-scheme
Framework-specific classes in views Use your own semantic classes

Quick Reference

Naming Conventions

.component           → .card, .btn, .badge
.component-sub       → .card-header, .card-body
.component-variant   → .card-sm, .btn-primary
.component-state     → .card-interactive (or .is-loading for global states)
.page-element        → .dashboard-grid, .settings-sidebar

Responsive Patterns

/* Mobile-first with container queries where possible */
.dashboard-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
}

@media (min-width: 768px) {
  .dashboard-grid {
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: var(--space-6);
  }
}

Browser Support

light-dark() requires Chrome 123+, Firefox 120+, Safari 17.5+.

Fallback for older browsers:

@media (prefers-color-scheme: dark) {
  :root {
    --color-surface: var(--color-zinc-800);
    --color-ink: var(--color-zinc-50);
  }
}

New Component Checklist

  • Created components/[name].css
  • Wrapped all rules in @layer components { }
  • All values reference design tokens (no magic numbers)
  • Uses semantic color tokens (not raw palette)
  • Class names are specific and won't collide
  • Subcomponents follow .component-sub naming
  • Dark mode works automatically (verified)
  • Imported in application.css if needed

Design Token Checklist

  • New tokens added to _global.css under :root
  • Color tokens use light-dark() for both modes
  • Raw palette values use OKLCH
  • Spacing follows the existing scale
  • Token names are semantic (--color-surface, not --color-white)

For detailed patterns and examples, see the references/ directory:

  • references/design-tokens.md — Full color palette (OKLCH), spacing, typography, naming conventions
  • references/components.md — Button, card, badge, alert, form, table patterns + Rails integration
  • references/dark-mode.mdlight-dark() deep dive, manual toggle, shadows, color-mix()
  • references/responsive.md — Mobile-first breakpoints, container queries, fluid typography, layout patterns
  • references/layers.md@layer cascade control, anti-patterns, third-party CSS
  • references/utilities.md — Complete recommended utility class set
Related skills

More from thinkoodle/rails-skills

Installs
4
GitHub Stars
4
First Seen
7 days ago