→ Document-level rules — the deliberate exception to Salty scoping everything by default.
Global Styles
Salty CSS scopes everything by default — every styled component and className gets a hashed class so its rules can't leak onto anything you didn't mean. Global styles are the deliberate exception: the handful of things that genuinely belong to the whole document rather than to one component. Base typography, the page background, how bare <a> links look, a reset — the stuff you'd otherwise dump into a top-level .css file.
So this is the escape hatch — the one that lets you slot all kinds of Jokers into your deck, forget they're in there, and then watch things stop working the way you were sure they would. (Shoutout to Balatro for breaking my brain a few times, the same way bad global CSS breaks a stylesheet.) This page is the how: where global styles live, how they sit in the cascade, one worked example, and — because there isn't much to build here so much as to get right — a clear line between what belongs in a global and what doesn't.
How global styles work, briefly
There are two places to declare them, and they take the exact same shape — pick whichever fits your project layout:
In a .css.ts file with defineGlobalStyles, exported so the build picks it up:
import { defineGlobalStyles } from "@salty-css/core/factories";
export const globalStyles = defineGlobalStyles({
body: { margin: 0 },
a: { color: "currentcolor" },
});Or in your config, via defineConfig({ global }) in salty.config.ts — handy when you'd rather keep base styles next to the rest of your setup.
One gotcha worth stating up front: a global styles file only does something if the build actually reaches it. In a .css.ts file that means exporting the result and making sure the file is in your build graph (imported somewhere the compiler sees, like everything else Salty compiles). Define a global, see nothing happen, and a stranded file is the usual culprit.
Where they land in the cascade
This is the part that saves you a debugging session later. Salty resolves conflicts with @layer, not selector specificity, and the layer order is:
imports, reset, global, templates, fonts, l0…l8Global styles live in the global layer. Two consequences fall straight out of that ordering:
- Your globals beat the reset.
globalsits abovereset, so overriding a reset default is just writing the value you want in your own global — no!important, no fight. - Component styles beat your globals.
styledandclassNamerules land inl0and up, which sit above global. So a broad global selector will never out-muscle a component, no matter how specific the selector looks. That's by design — it's the same guarantee that stops global CSS from leaking into your components.
Tokens and nesting work here exactly as they do inside styled and className — same parser, same surface. {theme.bg} resolves, & pre nests, media queries and pseudo-selectors all behave.
Define once, applies everywhere
Here's a realistic base: document-level styles plus one nested selector, using tokens.
import { defineGlobalStyles } from "@salty-css/core/factories";
export const globalStyles = defineGlobalStyles({
html: {
scrollBehavior: "smooth",
scrollPaddingTop: "5vh",
},
body: {
margin: 0,
fontFamily: "{font.main}",
background: "{theme.bg}",
color: "{theme.text}",
},
a: {
color: "currentcolor",
},
// Nested selectors compose with the parent, just like in a component.
blockquote: {
borderLeft: "3px solid {theme.altBackground}",
paddingLeft: "1rem",
"& cite": { color: "{theme.mutedText}" },
},
});Now the "use" side — and this is where global styles differ from the rest of the library. You don't import anything at the call site. There's no <GlobalStyles /> to render and no class to attach. Once the file is in the build graph, the rules are simply there, applied to every matching element in the document:
<body>
<a href="/salt">Any link on the page already inherits currentColor.</a>
<blockquote>No class attached — the rule just applies. <cite>the cascade</cite></blockquote>
</body>That's the whole model: you define the rules once against bare selectors, and the browser applies them everywhere those selectors match. No wiring per element.
A pragmatic utility or two
Global styles target bare HTML selectors, but the key can be any selector — including a class. Salty leans hard toward atomic components, and for anything reusable you'll almost always want className or a template instead. But a couple of genuinely document-wide utilities earn their place in a global — a visually-hidden helper, or a blanket list reset you don't want to think about again:
export const globalStyles = defineGlobalStyles({
// Reset list styling everywhere; opt back in per-component when you want it.
"ul, ol": { listStyle: "none", padding: 0 },
// A screen-reader-only helper.
".sr-only": {
position: "absolute",
width: "1px",
height: "1px",
overflow: "hidden",
clip: "rect(0, 0, 0, 0)",
},
});Define it once, then use it as a plain class in markup:
<span class="sr-only">Menu</span>The line to hold: reach for this for a small, stable set of document-wide helpers. The moment you're building a utility system in here, you're fighting the tool — that's className's job, and it keeps the styles scoped and typed.
What global styles are for — and what they're not
There isn't a long menu of examples for this page, because global styles are less about what to build and more about what belongs here. The honest version:
Good fits. Document base and rhythm — html/body setup, base font and background, ::selection, scroll-behavior. Bare-element defaults you want everywhere — link color inheritance, sensible <button> and form-control defaults. Print styles. A reset, or tweaks on top of Salty's.
Not this. Don't style components you own with global selectors — that's exactly the leak-and-specificity problem Salty exists to avoid. Reach for styled or className so the rules stay scoped to the thing they belong to. And don't try to override a component with a broad global selector: it sits in a lower layer and will lose the cascade every time. If a component is winning when you don't want it to, the tool is priority, not a global.
The rule of thumb: if a rule is about the document, it's a global; if it's about a component, it isn't. Every bare-element selector you add is a standing rule that touches every matching element on every page — powerful, and worth keeping short.
Global styles aren't the only global CSS
defineGlobalStyles owns the global layer, but it's not the only thing Salty emits into the shared, reusable, document-wide stylesheet — and reaching for a raw global when a dedicated factory exists is a common wrong turn:
- Design tokens belong in
defineVariables, which writes them as CSS custom properties on:root. Don't hand-roll a:root { --… }block in a global. @font-faceand font setup belong indefineFont— you get the global CSS plus a token you can reference, instead of a pasted font block.- Reusable style bundles belong in
defineTemplates, which get their own layer and can carry variants. - Third-party stylesheet imports belong in
defineImport, which land in theimportslayer — below everything, so they never accidentally beat your own styles.
Same idea each time: the dedicated factory emits cleaner global CSS, gives you types and token access, and lands in the right layer for free. Use defineGlobalStyles for the bare-selector rules that don't have a more specific home.
The built-in reset
Salty ships a small, opinionated CSS reset and applies it by default, in the reset layer (below your globals, so you can override any of it). Briefly, what it does: switches everything to border-box, zeroes default margins, calms a few html defaults (line-height, iOS text-size inflation, font smoothing), makes media elements (img, video, svg, canvas…) block-level and capped at their container width, lets long words break instead of overflowing, applies modern text-wrap to headings and paragraphs, and has links, buttons, and form controls inherit color and font instead of the browser's defaults.
You control it through the reset option:
defineConfig({ reset: "none" }); // bringing your own reset| Value | Behaviour |
|---|---|
'default' | Salty's built-in reset (the default if you omit the option). |
'none' | No reset — use this if you import a third-party reset yourself. |
| Custom CSS object | Same shape as GlobalStyles. |
If you're bringing your own reset, set reset: 'none' rather than layering two resets and hoping — you don't need both. For the full, line-by-line reset spec, see CSS reset.