Styles live next to the thing they style. Every Module and Component keeps a
single .scss file in its own directory, right beside its .js and (for views) its
.php. There is no central stylesheet you scroll through to find a rule — you open the
component.
How you use it
Give a component one SCSS file named after it, and pull it into the bundle from the
component's JS with a bare import:
// modules/Intro/Intro.js
import "./Intro.scss"
That import has no binding — it exists purely for its side-effect. Vite sees the edge from the JS chunk to the CSS and emits the styles as part of that component's chunk. Because components are presence-gated and code-split, a page only ever downloads the CSS for components it actually renders.
Why the import, not a manifest. Colocation plus the import side-effect makes CSS
follow the same per-component code-splitting as JS for free — no separate stylesheet
registry to keep in sync, and no styles shipped for components the page doesn't use.
The rule is simply: if a component has styles, its JS imports its .scss.
How selectors are structured
Scope everything under the component's root class and nest to mirror the markup. A
Module owns its block; elements inside it read as block__element; state and modifiers
hang off the block with &:
// modules/Intro/Intro.scss
@use 'lib/rhythm' as rhythm;
.intro {
@include rhythm.default(sm, md, none, none);
@include rhythm.apply();
.intro__item {
display: grid;
place-items: center;
&-content {
grid-area: pile;
z-index: 1;
}
&:hover {
.intro__item-content--static {
opacity: 0;
}
}
}
.navigation {
margin-top: 2rem;
}
}
Keep nesting shallow enough to read the shape of the markup at a glance, and lean on
& for state hooks (&:hover, &.is-animated) so a component's behavioural states sit
beside the base rules they modify.
The framework does not mandate a naming methodology. The block__element
convention above is what the codebase happens to use and what you'll see most often,
but BEM (or any other scheme) is not required — how you name selectors within your
component is your call. What is fixed is the structure: one colocated file per
component, scoped under its root class.
Global styles
A small amount of CSS can't be colocated because it isn't owned by any one component —
the reset, base element defaults, and shared @keyframes. That lives in
Crucial.scss and ships eagerly on the critical path:
// global/Crucial/Crucial.scss
@keyframes fade {
to {
opacity: 1;
translate: 0 0;
}
}
@layer reset {
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
// …a modern CSS reset
}
The reset is wrapped in @layer reset so component styles always win over it without a
specificity fight, and the globally-emitted fade keyframe is what the
animation mixin references by name.
Related
- The
lib/directory — shared mixins, functions, and variables you@use. - The rhythm system — the fluid vertical-spacing scale a Module applies.
- The state-class contract — how JS state reaches your SCSS.