styles/
main.scss entry: lists the stylesheets that emit CSS, in cascade order
abstracts/ Sass variables, mixins and functions; must not emit CSS
base/ global CSS: reset, grid, elements, helper classes
images/ images used by stylesheets
themes/<theme>/ theme.scss, the theme's custom property values
app/component/** component stylesheets, next to their components
All themes share one stylesheet. Most component styles are listed in styles/main.scss, because they use
global class names and their order matters. This is legacy: we're moving to CSS Modules, so don't add
component stylesheets to main.scss.
| What | Where |
|---|---|
| Styles for a new component | Component.module.scss next to it, imported by the component |
| Styles for an existing component | Its current stylesheet, or move it to a module when reworking it |
| Global element or helper styles | styles/base/ |
| A value that differs per theme | A custom property, see Themes |
| A value shared by stylesheets, the same in all themes | Sass variable in styles/abstracts/_variables.scss |
| A value used by one stylesheet | Local variable at the top of that file |
| A shared mixin | styles/abstracts/_mixins.scss |
| A page-level z-index | $global-zindex in styles/abstracts/_zindex.scss, see Z-Index |
| Third-party CSS | @import … layer(vendor) in styles/main.scss |
Every stylesheet that needs shared values starts with a relative @use '../../styles/abstracts' as *;,
so update the path when moving a stylesheet. Because of as *, a top-level $name: … with a name
abstracts defines changes the shared value for later stylesheets, so pick another name for local
variables.
Import a module with import styles from './Component.module.scss'; and read class names as written,
e.g. styles['route-name']. Webpack hashes them. Vitest doesn't compile Sass and returns a class name for
any key, so a test can import the module and check toHaveClass(styles['route-name']), but it won't catch
a misspelled name. Prefer testing what the user sees, as in Tests.
The layer order is declared once, at the top of styles/themes/default/theme.scss, which loads first:
@layer reset, vendor, tokens, theme;Later layers win regardless of specificity, and unlayered CSS beats every layer:
reset: element defaults (styles/base/_reset.scss).vendor: third-party CSS, currently Leaflet.tokens: default values from@hsl-fi/design-system-core.theme: the values in eachtheme.scss.- Unlayered: the app's own CSS. Avoid
!important, and keep it out of layers, where it inverts the order.
Use a custom property for values that vary by theme or at runtime, and a Sass variable for everything
else. var() doesn't work in media queries or data URIs, and Sass color functions can't take it.
A class is only unused if it appears nowhere at runtime, and it can come from more places than className:
| Source | Example | How to check |
|---|---|---|
| JS, configs, translations, SVGs | cx() keys, class="…" in icons and HTML strings |
git grep -nw <name>, which searches every tracked file |
| Built names | `message-bar-${type}`, lowercased enums, react-select's classNamePrefix |
Search the prefix or suffix and find the values it can take |
| Libraries at runtime | Leaflet's leaflet-popup-scrolled |
After yarn build, grep -lw <name> _static/js/*.js, then read each match. Find what triggers it, such as a prop |
| Other stylesheets | @extend |
git grep -nw <name> -- '*.scss' |
| HTML from outside the repo | CMS banners, third-party scripts | Can't be searched, so ask the content's owner |
The same goes for changing a rule: narrowing a selector or removing !important can make it lose to
another rule. To see what a change did, compare the compiled CSS per theme, then computed styles in a browser.