Agent Skills: Rails CSS Architecture Expert

Expert guidance for CSS architecture in Rails applications using modern CSS (no Tailwind). Use when writing stylesheets, organizing CSS files, implementing dark mode, defining design tokens, using CSS custom properties, creating component styles, working with CSS layers (@layer), using the light-dark() function, setting up color schemes, or structuring CSS for maintainability. Covers design tokens, semantic color systems, component-scoped styles, utility classes, responsive patterns, and file organization.

UncategorizedID: thinkoodle/rails-skills/css-architecture

Install this agent skill to your local

pnpm dlx add-skill https://github.com/ThinkOodle/rails-skills/tree/HEAD/skills/css-architecture

Skill Files

Browse the full folder contents for css-architecture.

Download Skill

Loading file tree…

skills/css-architecture/SKILL.md

Skill Metadata

Name
css-architecture
Description
Expert guidance for CSS architecture in Rails applications using modern CSS (no Tailwind). Use when writing stylesheets, organizing CSS files, implementing dark mode, defining design tokens, using CSS custom properties, creating component styles, working with CSS layers (@layer), using the light-dark() function, setting up color schemes, or structuring CSS for maintainability. Covers design tokens, semantic color systems, component-scoped styles, utility classes, responsive patterns, and file organization.

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
Rails CSS Architecture Expert Skill | Agent Skills