Home Benchmarks Learn Tools News
SPONSOR

AppSignal — Stop vibe-debugging. Every exception, every backtrace, grouped so you see patterns, not noise.

↗
Skills · Modern CSS
Front-end skill · v2 · 11 patterns

CSS without the tech debt.

Runs inside your agent. Enforces 11 modern CSS patterns. Replaces 12 legacy anti-patterns by default.

Install the skill → Read SKILL.md
11
Modern patterns
6
AI tools supported
0
Dependencies
agent — frontend-css live
$ agent write --skill frontend-css pricing.css
▼ generating pricing.css with skill loaded
native nesting (no Sass) USED
@container for component sizing USED
:has() for state targeting USED
oklch() + color-mix() tokens USED
logical properties (margin-block...) USED
@layer cascade order set USED
scroll-driven animation + fallback USED
written pricing.css · 1.2 KB · 0 anti-patterns A
→ ready to ship. open in editor? [Y/n]
Works with
  • Cursor
  • Claude Code
  • Codex CLI
  • Windsurf
  • GitHub Copilot
  • Gemini CLI
What it enforces

Native CSS, in production-grade form.

frontend-css / patterns SKILL.md · 11 patterns · baseline 2026
patterns/
nesting.diff •
−before.scss Before
// pricing.scss — needs Sass to compile
.card {
padding: 24px;
.card__title { font-size: 20px; }
&:hover { background: #1a1a1a; }
}
+after.css After
/* pricing.css — native, no build step */
.card {
padding: var(--space-lg);
 
& .card__title { font-size: var(--text-lg); }
&:hover { background: var(--color-bg-elevated); }
}
− sass-loader · postcss · build step → + ships as-is · zero deps · 0 ms build
container-queries.diff •
−before.css Before
/* breaks in narrow containers, awkward to override */
.card { grid-template-columns: 1fr; }
 
@media (width >= 768px) {
.card { grid-template-columns: 200px 1fr; }
}
+after.css After
.card-wrapper { container-type: inline-size; }
 
.card {
grid-template-columns: 1fr;
 
@container (width >= 400px) {
grid-template-columns: 200px 1fr;
}
}
− breaks at 768 px in sidebars → + adapts to its container, anywhere it lives
has-selector.diff •
−focus-state.js Before
// 4 event handlers per field
input.addEventListener('focus', () =>
input.parentElement.classList.add('focused'));
input.addEventListener('blur', () =>
input.parentElement.classList.remove('focused'));
 
/* CSS */
.form-group.focused { border-color: var(--accent); }
+focus-state.css After
.form-group:has(input:focus) {
border-color: var(--accent);
}
 
.field:has(input:not(:placeholder-shown)) .field__label {
transform: translateY(-100%) scale(0.8);
}
− 8 lines of JS · 4 event handlers · bundle weight → + 0 lines of JS · declarative, server-renderable
modern-color.diff •
−tokens.css Before
:root {
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
}
 
.surface { background: #f9fafb; color: #111827; }
 
@media (prefers-color-scheme: dark) {
.surface { background: #111827; color: #f9fafb; }
}
+tokens.css After
:root { color-scheme: light dark; }
 
.btn { background: oklch(55% 0.2 250); }
.btn:hover {
background: color-mix(in oklch, oklch(55% 0.2 250) 85%, white);
}
 
.surface {
background: light-dark(var(--gray-50), var(--gray-900));
color: light-dark(var(--gray-900), var(--gray-50));
}
− 2 hex per color · doubled rules per theme → + one declaration · both themes · derived hovers
Benchmarked

Proof, not vibes.

Pricing Section brief · Claude Opus 4.7 · with skill vs. without · 2026-04-21
100
Performance 84 → 100 baseline +16
0
Anti-patterns 8 → 0 baseline −8
7
Modern features Avg adopted per file
94
CSS grade · A vs C · 71 baseline +23
0–49 50–89 90–100
Single-run comparison. Same model and prompt, scored on Lighthouse + an internal rubric.
Install

One file. Six tools. Zero ceremony.

One Markdown file, zero dependencies. Pick your tool below.

1Drop this in

Project: .cursor/skills/frontend-css.md

2Or fetch it directly
curl -fsSL https://webdeveloper.com/skills/frontend-css/SKILL.md -o .cursor/skills/frontend-css.md

Restart Cursor. Every CSS file the agent writes from now on uses the modern patterns and avoids the listed anti-patterns.

1Drop this in

User-level: ~/.claude/skills/frontend-css/SKILL.md

2Or fetch it directly
mkdir -p ~/.claude/skills/frontend-css && curl -fsSL https://webdeveloper.com/skills/frontend-css/SKILL.md -o ~/.claude/skills/frontend-css/SKILL.md

Claude Code auto-discovers skills in ~/.claude/skills/. Available across every project on this machine.

1Drop this in

Project: AGENTS.md (append the SKILL contents)

2Or fetch it directly
curl -fsSL https://webdeveloper.com/skills/frontend-css/SKILL.md >> AGENTS.md

Codex CLI reads AGENTS.md automatically when you run it from the project root.

1Drop this in

Project: .windsurf/rules/frontend-css.md

2Or fetch it directly
mkdir -p .windsurf/rules && curl -fsSL https://webdeveloper.com/skills/frontend-css/SKILL.md -o .windsurf/rules/frontend-css.md

Windsurf loads project rules on every Cascade run.

1Drop this in

Project: .github/copilot-instructions.md (append)

2Or fetch it directly
mkdir -p .github && curl -fsSL https://webdeveloper.com/skills/frontend-css/SKILL.md >> .github/copilot-instructions.md

Copilot reads .github/copilot-instructions.md as project-wide context.

1Drop this in

Project: .gemini/skills/frontend-css.md

2Or fetch it directly
mkdir -p .gemini/skills && curl -fsSL https://webdeveloper.com/skills/frontend-css/SKILL.md -o .gemini/skills/frontend-css.md

Gemini CLI auto-loads project skills on the next run.

The full SKILL.md

543 lines · plain Markdown · MIT-licensed
SKILL.md
---
name: frontend-css
description: >-
  Enforces modern CSS patterns and prevents legacy anti-patterns when writing
  stylesheets. Use when generating CSS, styling components, writing media
  queries, adding animations, or working with colors and layout.
---

# Modern CSS

Write CSS using native modern features. Every pattern below has baseline
cross-browser support (Chrome, Firefox, Safari, Edge) unless noted otherwise.
Prefer native CSS over preprocessors, libraries, and JavaScript workarounds.

All visual values (colors, spacing, typography, motion) must come from design
tokens defined in the `frontend-design-system` skill — never hardcode values
in component styles.

## Nesting

Use native CSS nesting. Do not add Sass, Less, or PostCSS nesting plugins.

```css
.card {
  padding: var(--space-lg);

  & .card__title {
    font-size: var(--text-lg);
  }

  &:hover {
    background: var(--color-bg-elevated);
  }

  @media (width < 600px) {
    padding: var(--space-md);
  }
}
```

**Maximum nesting depth: 3 levels.** Deep nesting creates specificity problems
and makes styles harder to override. If you need deeper nesting, flatten with
a new class — that's a signal the component should be broken apart.

## Container Queries

Use `@container` for component-level responsiveness. `@media` queries check
the viewport, but a component doesn't know if it's in a full-width layout or
a narrow sidebar. Container queries solve this.

**Reserve `@media` for page-level layout shifts only** (switching from
sidebar to stacked layout). Use `@container` for everything else.

```css
.card-wrapper {
  container-type: inline-size;
}

.card {
  display: grid;
  grid-template-columns: 1fr;

  @container (width >= 400px) {
    grid-template-columns: 200px 1fr;
  }
}
```

The `container-type` must be on an ancestor, never on the element being
queried. The query measures the container's available space, not the
element's own width.

## :has() Selector

`:has()` enables parent-aware and sibling-aware styling without JavaScript
class toggling. It replaces the most common remaining use case for JS in
styling.

```css
.form-group:has(input:focus) {
  border-color: var(--color-accent);
}

.field:has(input:not(:placeholder-shown)) .field__label {
  transform: translateY(-100%) scale(0.8);
}

.card:has(img) {
  grid-template-rows: 200px 1fr;
}
```

`:has()` is powerful but expensive to evaluate at scale. Avoid using it in
selectors that match many elements on the page (e.g., `*:has(...)` or
`div:has(...)`). Scope it to specific component classes.

## CSS Layers

Use `@layer` to control cascade priority explicitly. This eliminates
specificity wars and makes the order of `<link>` tags irrelevant.

Define the layer order once at the top of the main stylesheet:

```css
@layer reset, base, components, utilities;

@layer reset {
  *, *::before, *::after { box-sizing: border-box; margin: 0; }
}

@layer base {
  body { font-family: var(--font-sans); }
}

@layer components {
  .btn { padding: var(--space-sm) var(--space-lg); }
}

@layer utilities {
  .visually-hidden {
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
  }
}
```

Styles outside any `@layer` always beat layered styles. Never put component
styles outside a layer — it defeats the purpose of the cascade control.

## Scroll-Driven Animations

Use native scroll-driven animations instead of JavaScript scroll libraries.
Supported in Chrome, Edge, and Safari. Firefox is in active development
(Interop 2025/2026 focus area) — provide a static or IntersectionObserver
fallback for Firefox until it ships.

```css
@keyframes fade-in {
  from { opacity: 0; translate: 0 2rem; }
  to { opacity: 1; translate: 0 0; }
}

.reveal {
  animation: fade-in linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}
```

**Fallback pattern**: elements using `animation-timeline: view()` should have
their final visual state as the default (opacity: 1, translate: 0), and the
animation should enhance by starting from the hidden state. This way,
unsupported browsers show the content normally — they just don't get the
scroll animation.

```css
.progress-bar {
  animation: grow-width linear both;
  animation-timeline: scroll(root);
}

@keyframes grow-width {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}
```

## View Transitions

Use the View Transitions API for page-to-page and state-change animations.

For multi-page apps (MPA), opt in via meta tag and CSS:

```css
@view-transition {
  navigation: auto;
}
```

Name elements that should animate between pages:

```css
.hero-image {
  view-transition-name: hero;
}
```

Every `view-transition-name` must be unique on the page. Two elements with
the same name will break the transition. For lists, generate unique names
dynamically with custom properties or inline styles.

For single-page state changes, call `document.startViewTransition()` in JS
before updating the DOM.

## Entry Animations with @starting-style

Use `@starting-style` to animate elements on entry (when they first render
or transition from `display: none`). This replaces the common hack of
adding an animation class via `requestAnimationFrame`.

```css
dialog {
  opacity: 1;
  translate: 0 0;
  transition: opacity var(--transition-base), translate var(--transition-base);

  @starting-style {
    opacity: 0;
    translate: 0 1rem;
  }
}

[popover] {
  opacity: 1;
  transition: opacity var(--transition-fast), display var(--transition-fast) allow-discrete;

  @starting-style {
    opacity: 0;
  }
}
```

Pair with `allow-discrete` on the `transition` shorthand when transitioning
`display` or `overlay` properties.

## Modern Color

Use `oklch()` as the default color space. Its perceptually uniform lightness
means a value of 50% looks equally bright regardless of hue — making palette
scales predictable. See the `frontend-design-system` skill for the full
color token architecture.

```css
:root {
  --color-primary: oklch(55% 0.2 250);
  --color-primary-light: oklch(75% 0.15 250);
  --color-primary-dark: oklch(35% 0.2 250);
}
```

Use `color-mix()` for dynamic variants:

```css
.btn:hover {
  background: color-mix(in oklch, var(--color-primary) 85%, white);
}
```

Use `light-dark()` for theme-aware values without duplicating declarations:

```css
:root { color-scheme: light dark; }

.surface {
  background: light-dark(var(--gray-50), var(--gray-900));
  color: light-dark(var(--gray-900), var(--gray-50));
}
```

## Logical Properties

Use logical properties instead of physical directional properties. This
makes styles work in both LTR and RTL layouts without overrides.

| Physical (avoid) | Logical (use) |
|-------------------|---------------|
| `margin-left` | `margin-inline-start` |
| `margin-right` | `margin-inline-end` |
| `padding-top` | `padding-block-start` |
| `padding-bottom` | `padding-block-end` |
| `width` | `inline-size` |
| `height` | `block-size` |
| `top` | `inset-block-start` |
| `left` | `inset-inline-start` |
| `border-left` | `border-inline-start` |
| `text-align: left` | `text-align: start` |

Shorthand: `margin-inline`, `padding-block`, `inset` for symmetric values.

## Subgrid

Use `subgrid` when child elements need to align to an ancestor grid.

```css
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--space-xl);
}

.card {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 3;
}
```

This ensures card headers, bodies, and footers align across columns
regardless of content length. Without subgrid, each card's internal grid
is independent and row heights won't match.

## Anchor Positioning

Use CSS anchor positioning for tooltips, popovers, and floating UI instead
of JavaScript positioning libraries (Floating UI, Popper).

```css
.trigger {
  anchor-name: --tooltip-anchor;
}

.tooltip {
  position: fixed;
  position-anchor: --tooltip-anchor;
  inset-block-end: anchor(top);
  inset-inline-start: anchor(center);
  translate: -50% -0.5rem;
  position-try-fallbacks: flip-block, flip-inline;
}
```

`position-try-fallbacks` handles cases where the tooltip would overflow the
viewport — it automatically flips the position. This replaces the most
complex part of JS positioning libraries.

## Selectors and Specificity

Prefer low-specificity selectors. A single class is the sweet spot — specific
enough to target the element, low enough to override easily.

- No ID selectors in stylesheets (`#header`)
- No deeply nested selectors (`.page .section .content .card .title`)
- No `!important` except in the `utilities` layer
- No qualifying classes with elements (`div.card`) — just `.card`

## Units

| Context | Unit | Rationale |
|---------|------|-----------|
| Font sizes | `rem` | Respects user's browser font size preference |
| Spacing / padding / margins | `rem` or tokens | Scales proportionally with font size |
| Line height | Unitless (`1.5`) | Inherits correctly to children |
| Border / outline widths | `px` | Sub-pixel rendering is expected |
| Media / container queries | Range syntax (`width >= 600px`) | Clearer than `min-width` |
| Zero values | No unit (`0`) | `0px` and `0rem` are identical noise |
| Viewport-relative | `dvh` / `dvw` | Accounts for mobile browser chrome |

## Anti-Patterns

**Never do these:**

- Use a preprocessor for nesting — native nesting is baseline supported
- Write `@media` queries for single-component responsiveness — use
  `@container` queries
- Toggle parent classes from JavaScript for styling — use `:has()`
- Add `!important` outside of a utility layer — it starts specificity wars
  that escalate until everything is `!important`
- Use `height: 100vh` on mobile — the mobile browser chrome makes `100vh`
  taller than the visible area; use `100dvh`
- Import animation libraries for scroll effects — use `animation-timeline`
- Hardcode color values — use design tokens from `frontend-design-system`
- Use `rgb()` or `hsl()` for new colors — use `oklch()` for perceptual
  uniformity
- Write physical properties (`left`, `right`, `top`, `bottom`) — use
  logical properties for RTL compatibility
- Set `z-index: 9999` — use the z-index scale from `frontend-design-system`
- Use `float` for layout — use grid or flexbox
- Nest selectors beyond 3 levels — flatten with a new class
Pair it

Stack it with the rest of the suite.

Front-end04 Design Tokens

Two-tier token architecture, spacing and type scales, oklch color system, dark/light theming, motion tokens, and z-index scales.

↗
Front-end02 Components

Semantic HTML, ARIA widget patterns, native dialog and Popover API, heading hierarchy, progressive enhancement, BEM naming.

↗
Review07 Pre-Deploy Review

26-point audit of error handling, debug artifacts, hallucinated APIs, and a11y smells before you ship.

↗

Changelog

V2 April 9, 2026
Added @starting-style for entry animations, light-dark() function, anchor positioning fallbacks with position-try-fallbacks, scroll-driven animation fallback strategy, @container vs @media decision guidance, and cross-references to Design Tokens skill.
V1 April 3, 2026
Initial skill covering native nesting, container queries, :has(), CSS layers, scroll-driven animations, view transitions, oklch colors, logical properties, subgrid, and anchor positioning.

FAQ

What modern CSS features does this skill cover?

Native CSS nesting, container queries, the :has() selector, CSS layers (@layer), scroll-driven animations, view transitions, @starting-style entry animations, oklch() colors with color-mix() and light-dark(), logical properties, subgrid, and anchor positioning. All features have baseline 2026 cross-browser support or include noted fallbacks.

How does this skill handle browser compatibility?

Every pattern targets baseline 2026 support across Chrome, Firefox, Safari, and Edge. Where a feature is still rolling out to one browser (such as scroll-driven animations in Firefox), the skill notes it and recommends a reasonable fallback like IntersectionObserver or a static end-state.

Can I use this skill with Sass or Tailwind?

This skill prioritizes native CSS and discourages adding preprocessors solely for features like nesting, which is now supported natively. You can still use Sass or Tailwind alongside this skill, but it will guide your AI coding tool to prefer native CSS alternatives whenever possible.

Which AI coding tools is this compatible with?

Cursor, Claude Code, Codex CLI, Windsurf, GitHub Copilot, and Gemini CLI. The skill is a single Markdown file and ships in the native format for each tool with one-click copy.

STATUS ● BUILDING THE FUTURE
MISSION MAKE AI SHIP BETTER CODE.
VERSION BETA 3.0

MAKE AI SHIP BETTER CODE.

@WEBDEVELOPERHQ ↗
TERMS / PRIVACY
FRIENDS
Authentic Jobs ↗
Web Reference ↗
Ready.dev ↗
Fullres ↗
© 2026 WEB DEVELOPER / ALL RIGHTS RESERVED