CSS, Layers, and Container Queries
CSS, the language people are routinely told to “stop fighting and just use a runtime for”, has changed more in five years than the previous fifteen. Here’s the short list of what’s now native and broadly supported.
@layer — explicit specificity ordering
Section titled “@layer — explicit specificity ordering”Specificity used to be the cause of most CSS suffering. @layer makes it controllable.
@layer reset, tokens, base, components, utilities, overrides;
@layer reset { /* normalize */ }@layer tokens { :root { --color-accent: oklch(57% 0.18 248); } }@layer base { body { font-family: system-ui; } }@layer components { .card { padding: 1rem; } }@layer utilities { .mt-4 { margin-top: 1rem; } }@layer overrides { /* anything here wins */ }Selectors in later layers always beat selectors in earlier layers — independent of specificity score. Want utility classes that beat component styles? Order the layers. No !important.
Kitsune publishes the convention kit.reset, kit.tokens, kit.base, kit.components, kit.utilities, app.overrides so app code always wins over kit defaults.
@container — responsive components
Section titled “@container — responsive components”Media queries respond to the viewport. Container queries respond to a parent element’s size.
.card-grid { container-type: inline-size; }
.card { padding: 0.5rem; }
@container (min-width: 32rem) { .card { padding: 1rem; display: grid; grid-template-columns: 1fr 2fr; }}The .card style now responds to whether its grid container is at least 32rem wide. Drop the same component into a sidebar and a main column; it adapts to each.
This is what makes “true reusable components” possible without prop-drilling responsive variants.
:has() — parent selectors
Section titled “:has() — parent selectors”/* Style a card differently if it contains a footer */.card:has(> footer) { padding-block-end: 0; }
/* Style a form differently if it has any invalid input */form:has(:invalid) button[type=submit] { opacity: 0.5; }
/* Hide a sidebar header if the sidebar is empty */.sidebar > header:has(+ :empty) { display: none; }This is enormous. It removes most of the JavaScript people write to add and remove classes based on DOM structure.
oklch() and color-mix()
Section titled “oklch() and color-mix()”Perceptually uniform colors and runtime mixing.
:root { --accent: oklch(57% 0.18 248); --accent-dim: color-mix(in oklch, var(--accent), transparent 70%); --accent-hover: color-mix(in oklch, var(--accent), white 10%);}You can compute hover, disabled, and dim variants from a single token without precomputing a palette.
Native nesting
Section titled “Native nesting”.card { padding: 1rem; & h2 { margin: 0 0 0.5rem; } &:hover { border-color: var(--accent); } & + & { margin-top: 0.5rem; }}This is the entire reason most teams reached for Sass. It’s now native. Vite, esbuild, and every modern bundler understand it without a preprocessor.
light-dark() — automatic theming
Section titled “light-dark() — automatic theming”:root { color-scheme: light dark; }
body { background: light-dark(white, oklch(20% 0 0)); color: light-dark(black, oklch(95% 0 0));}Add color-scheme: light dark to a root element, then use light-dark() to declare both modes inline. The browser applies the right one based on the user’s preference. No JavaScript theme switcher unless you want manual override.
What this means for Kitsune
Section titled “What this means for Kitsune”Kitsune theme tokens are CSS custom properties. The cascade applies them. Components consume them. There’s no styling runtime.
:root { --kit-color-accent: oklch(57% 0.18 248); --kit-radius-md: 0.5rem; --kit-space-3: 0.75rem;}Inside a component:
:host { background: var(--kit-button-bg, var(--kit-color-accent)); border-radius: var(--kit-radius-md);}Theme switching is applyTheme(brandTheme) setting custom properties on the root element. Container queries handle responsive variants. light-dark() handles dark mode. The browser does all of it.
When you don’t need a CSS-in-JS runtime, you don’t need any of the build-time machinery, type-generation, or runtime-evaluation that comes with one. You ship 0 KB of styling runtime and the cascade does its job.