Get started Layered modules
/anyblades/blades/tree/main/css/layered
Blades CSS, whole or part by part, is also available wrapped in @layer blades { }, so any unlayered styles of yours win over it without specificity fights.
Whole framework #
blades.layered.css / blades.layered.min.css is blades.css wrapped in @layer blades { }:
<link rel="stylesheet" href="
https://cdn.jsdelivr.net/npm/@anyblades/blades@3/css/layered/blades.layered.min.css
"/>
When you can use @import, @import url("…/blades.min.css") layer(blades); does the same without this file.
Per-part imports #
With bundlers and component frameworks (SvelteKit, Next.js, Nuxt, Astro, …) #
Import the parts from JavaScript, i.e. the root layout and the components that use them, rather than with @import inside a component's <style> block. The parts, @anyblades/blades/<part>, come wrapped in @layer blades, which a JS import can't add by itself.
// root layout, once: the base and every part that others build on
import "@anyblades/blades/variables";
import "@anyblades/blades/layout/document";
import "@anyblades/blades/content/typography";
import "@anyblades/blades/content/button";
import "@anyblades/blades/forms/basics";
// in the component that renders a <dialog>
import "@anyblades/blades/components/card";
import "@anyblades/blades/components/modal";
import "@anyblades/blades/components/modal-modern"; // optional: open animation without JS
<style>
/* unlayered, so it wins over Blades whatever the specificity (except Blades' !important) */
article {
max-width: 40rem;
}
</style>
- Bundlers dedupe JS-side CSS imports and split them per route, so each part is downloaded and cached once and only where it's used.
- An
@importinside a scoped<style>(Svelte, Astro, Vuescoped) is processed as that component's own styles. The rules get scoped to the component's markup (Svelte also prunes the unused ones) and are duplicated in every component that imports them.
Framework examples #
A confirm dialog component, with the root layout importing the base parts as above. The component imports card and modal and overrides Blades in its own styles. Blades' dialog > article > footer button:not(:first-of-type) is more specific than the component's rules, but it is in @layer blades, so the component's rules win. With the non-layered source (@anyblades/blades/src/components/modal), the second button would keep Blades' margin-left.
Svelte, Confirm.svelte:
<script>
import "@anyblades/blades/components/card";
import "@anyblades/blades/components/modal";
</script>
<dialog open>
<article>
<p>Delete this item?</p>
<footer>
<button class="secondary">Cancel</button>
<button>Delete</button>
</footer>
</article>
</dialog>
<style>
footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
button {
margin: 0;
}
</style>
Vue (<script setup> and <style scoped>) and Astro (the imports in the frontmatter, <style> in the component) work the same way.
React, Confirm.jsx with a CSS module:
import "@anyblades/blades/components/card";
import "@anyblades/blades/components/modal";
import styles from "./Confirm.module.css";
export default function Confirm() {
return (
<dialog open>
<article>
<p>Delete this item?</p>
<footer className={styles.footer}>
<button className="secondary">Cancel</button>
<button>Delete</button>
</footer>
</article>
</dialog>
);
}
/* Confirm.module.css */
.footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
.footer button {
margin: 0;
}
Per-page CSS in a SvelteKit production build of this example, next to the whole blades.layered.min.css (gzip sizes in parentheses):
| CSS | |
|---|---|
blades.layered.min.css, for comparison |
76.9 kB (12.4 kB) |
| Every page: the root layout's parts | 25.5 kB (4.7 kB) |
The page with Confirm: plus card, modal and the component's styles |
+ 4.7 kB (+ 1.4 kB) |
The sizes are from a build with build: { cssTarget: "esnext" } in vite.config.js. With Vite's default browser targets, light-dark() is lowered to fallback custom properties, which adds about 4 kB to the root layout's CSS.
Available parts #
@anyblades/blades/<part> is the file under
css/layered/:
- Pico's parts:
variables,layout/*,content/*,forms/*,components/*andutilities. A whole category is one import, e.g.@anyblades/blades/components(Pico's parts of that category only;layoutwithoutlandmarks). - Blades' own parts, mostly in the same folders as Pico's:
components/modal-modernadds the CSS-only open animation forshowModal()on top ofcomponents/modal(No-JS transitions)components/dropdown-popoverstyles<ul popover>/<menu popover>menus opened by a button (Modern Popover API)variables-blades,layout/columns,content/code-block,content/icon,forms/float-label,components/timelinecompact: import it last (seeCompact sizing)
- The same names under
@anyblades/blades/src/…are the sources without the layer, e.g.@anyblades/blades/src/components/card, to put them in a layer of your own (@import "@anyblades/blades/src/components/card" layer(ui);) or to run your own build.@anyblades/blades/srcon its own is the source of the wholeblades.css.
Order and dependencies #
-
variablesandlayout/documentare the minimum. Inside the layer, later parts still win over earlier ones, so keep the order ofsrc/pico/_index.css(variables → layout → content → forms → components → utilities). Blades' own parts come after Pico's, in the order ofsrc/blades/_index.css, withcompactlast. -
Parts don't
@importthe parts they build on:components/modalexpectscomponents/cardcomponents/groupandcomponents/dropdownexpectcontent/buttonandforms/basicscomponents/modal-modernexpectscomponents/modalcomponents/dropdown-popoverexpectscomponents/dropdown- the
forms/*extras expectforms/basics
If they did, every route chunk would carry its own copy. Import those shared parts once, at the root or next to the part that needs them; bundlers dedupe the imports.
-
Landmark padding:
pico.cssandblades.ninja.csspadbody > header,body > mainandbody > footerlike Pico, which doesn't match when the framework wraps the page in an element (SvelteKit's<div style="display: contents">, Next.js pages router#__next, Nuxt#__nuxt, a SPA's#root/#app).blades.cssdoesn't pad them; addlayout/landmarksfor Pico'sbody >padding.
Overrides and caveats #
Override in your component's <style> or app.css, or set --pico-* variables on :root or on an element. Unlayered CSS always wins over @layer blades. The usual layer caveats apply:
- Blades' own
!importantdeclarations (form validation, reduced motion) beat your unlayered!important. - Your
@keyframesof the same name as Blades' win regardless of load order. - If you use other layered CSS, declare the order once, before anything else is loaded, because the first
@layerthe browser sees fixes the order. For Tailwind v4 that is@layer theme, base, blades, components, utilities;at the top of the CSS file you import first (before@import "tailwindcss"), and that file has to be imported before any Blades part.