Coexistence with existing CSS
Most pages that adopt a framework are not empty. There is a theme, a legacy stylesheet, a plugin that styles button, and a deadline. This page is about that situation: what iVOLT does to stay out of the way, what it does not do, and the exact rules that decide who wins when two stylesheets disagree.
The short version: .iv-root limits where element styles apply, prefixes prevent name collisions, and cascade layers order iVOLT's own rules. Layers do not isolate the framework from your CSS, and it is important to understand why before you debug a surprise.
Scope and prefixes
Every element selector in base.css starts with .iv-root. Outside that class iVOLT styles no h1, no a, no input. You choose the scope:
<!-- whole page -->
<html lang="en" class="iv-root" data-iv-theme="system">
<!-- one region of an existing site -->
<div class="iv-root" data-iv-theme="light">
<!-- iVOLT markup here; the surrounding page is untouched -->
</div>Everything else is prefixed and a contract test enforces it: classes start with iv- (iv-button, iv-card), utilities with iv-u-, custom properties with --iv-, data attributes with data-iv-, and the IIFE build exposes exactly one global, IVOLT. A collision with your own .button or --color-primary is therefore not possible by construction.
Two caveats. .iv-root also sets box-sizing: border-box on itself and all its descendants, so third-party widgets inside the scope inherit that; and the theme scope re-applies color and background-color on any element carrying data-iv-theme, which is what lets a dark island sit inside a light page.
The cascade-layer rules, exactly
The layered entry declares its order up front and assigns a layer at import time:
@layer iv.reset, iv.tokens, iv.base, iv.layout, iv.components, iv.utilities, iv.overrides;
@import url("./tokens.css") layer(iv.tokens);
@import url("./base.css") layer(iv.base);
/* … */Source modules declare no layer of their own, which is what lets the same file serve the layered build, the flat build and a per-component import. Three rules follow, and the third one is the one that bites:
- Inside layers, later wins.
iv.utilitiesbeatsiv.componentseven with lower specificity. That is whyiv-u-gap-4reliably overrides a component modifier. - Any normal (non-
!important) unlayered declaration beats every normal layered declaration, at any specificity. A singlebutton { background: red }in a legacy theme overrides.iv-buttoninivolt.css. This is not a bug in iVOLT or in your browser; it is how the cascade orders unlayered rules — as if they were in a final, implicit layer. - Among
!importantdeclarations the order reverses completely. Early layers win and unlayered declarations become the weakest. Consequence:.iv-u-sr-only, the one rule in the package that uses!important, cannot be overridden from your unlayered CSS inivolt.css— while inivolt.flat.cssit can.
So layers order iVOLT internally and make it easy for you to override; they do not defend it. Say it out loud once and the debugging gets much shorter.
Choosing a strategy
| Your situation | Use | Why |
|---|---|---|
| New page, you control the CSS | css/ivolt.css | Layered order, easy overrides from your own unlayered rules |
| Legacy stylesheet you cannot edit | css/ivolt.flat.css | No @layer: it competes by specificity and source order like any other sheet |
| Legacy stylesheet you can import | ivolt.css + wrap the legacy sheet in a layer | Puts the old CSS under iVOLT's precedence without rewriting it |
The wrapping trick, with the layer order declared before iVOLT's so the legacy rules sit underneath:
@layer legacy, iv.reset, iv.tokens, iv.base, iv.layout, iv.components, iv.utilities, iv.overrides;
@import url("legacy-theme.css") layer(legacy);
@import url("ivolt/css/ivolt.css");Once the legacy sheet is in a layer, its unlayered privilege is gone and iVOLT's components win. Your own new rules, written unlayered, still win over both — which is usually what you want.
To override deliberately inside the layered build, the iv.overrides layer exists for exactly that and is declared last:
@layer iv.overrides {
.iv-button { border-radius: 0; }
}No rule in the package uses !important apart from .iv-u-sr-only, and component selectors stay at or below specificity (0,2,0) — both enforced by contract tests — so a plain class of your own is enough to win when the layering allows it.
The optional reset
css/reset.css ships unlayered on purpose. Linked as-is it would sit outside every layer and, by rule 2 above, beat the components it is supposed to sit underneath. Use one of the two supported forms:
@import url("ivolt/css/reset.css") layer(iv.reset); /* assign the layer yourself */
<link rel="stylesheet" href="ivolt/css/reset.layer.css"> <!-- already wrapped -->On an existing site you usually want no reset at all: the site already has one, and a second normalisation is how unexplained spacing changes start. The base styles do not depend on it — .iv-root sets its own box-sizing so the guarantee holds either way.
WordPress and other PHP sites
There is no build step to add. Copy packages/ivolt/dist into the theme (for example assets/ivolt/) and enqueue the files.
function theme_ivolt_assets() {
$base = get_template_directory_uri() . '/assets/ivolt';
wp_enqueue_style('ivolt', $base . '/css/ivolt.flat.css', array(), '0.5.0-beta');
wp_enqueue_script('ivolt', $base . '/js/auto.js', array(), '0.5.0-beta', true);
}
add_action('wp_enqueue_scripts', 'theme_ivolt_assets');Notes specific to this environment:
- Prefer the flat build. Themes and plugins almost never use layers, so under
ivolt.csstheir unlayered element rules would override iVOLT everywhere. - Put
iv-rooton a container, not on<html>, unless you are converting the whole theme. A wrapper around the content area lets the admin bar, the plugin widgets and the old templates keep their styles. - Register the script as a module if you use
auto.js(WordPress needstype="module", which recent versions support through script strategies or ascript_loader_tagfilter). If that is inconvenient, enqueuejs/ivolt.iife.min.jsinstead and callIVOLT.init()yourself — it initialises nothing on load. - There is no class scanner and no purge step. The utility matrix is finite and fully generated, so classes written inside PHP templates, shortcodes, the block editor or a database field all work. Nothing has to see your markup at build time.
- Editor content. If post content is rendered inside
.iv-root, headings, lists, tables and code in that content get the base styles. That is usually desirable; if it is not, putiv-rooton the chrome rather than on the entry body.
Astro, Vite and other bundlers
Install the tarball produced by npm pack inside packages/ivolt and import through the package exports. Deep paths into dist/ are not part of the API and may move.
import "@intervolutions/ivolt/css/ivolt.css";
import { init } from "@intervolutions/ivolt";
if (typeof document !== "undefined") init(document);What matters when a framework renders on the server:
- No module touches
document,window,navigatorormatchMediaat import time, so importing the package during SSR does not throw. A unit test asserts this in a Node environment. - Importing
@intervolutions/ivolthas no side effects. Only@intervolutions/ivolt/autoinitialises on load, and it checks for a document first. Prefer the explicitinit()in a client script when the framework controls hydration. init()is idempotent and only upgrades elements marked withdata-iv-component, so calling it again after a client-side navigation or a partial re-render is safe. When you tear down a region, calldestroy()so attributes are restored.- The package is side-effect annotated, so a bundler tree-shakes the components you never import; the pack smoke test verifies that.
- Islands. Markup rendered by a component that is never hydrated still works for the CSS-only families, and keeps the
<details>and:targetfallbacks for the interactive ones.
Content-Security-Policy
The package runs under script-src 'self'. It contains no eval, no new Function, no innerHTML with external data and no on* attributes, and it makes no network requests, loads no remote fonts and sends no telemetry.
One exception is worth planning for. If you use the theme pre-read snippet — the tiny inline script that reads the stored preference before first paint, so the page does not flash the wrong theme — it is inline by necessity, and a strict policy needs its hash or a nonce:
<script>try{document.documentElement.classList.add("docs-js");var t=localStorage.getItem("iv-theme");if(t==="light"||t==="dark"||t==="system")document.documentElement.setAttribute("data-iv-theme",t)}catch(e){}</script>Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-jUAUQ8+jWVPo7EbSE9FVWWmn/FVSalWrOarVKCFs7BU='; style-src 'self'The hash above is computed at build time from the exact snippet this site injects (sha256-jUAUQ8+jWVPo7EbSE9FVWWmn/FVSalWrOarVKCFs7BU=), so the two cannot drift apart.
This documentation publishes the hash for the snippet it ships. If you modify the snippet by one character, the hash changes — regenerate it, or serve the script with a nonce instead. Skipping the snippet entirely is also a valid choice: the theme then applies after the stylesheet and the script have run.
The only dynamic styling the package does is setting custom properties through style.setProperty, which counts as an inline style attribute. If your style-src forbids those, that is the thing to test.