Taming third-party CSS

You want to drop a legacy library into a cascivo app — a heavy charting widget, a drag-and-drop kanban board, a rich-text editor — and it ships its own global stylesheet. This page is the one-recipe answer.

The problem

cascivo ships every rule inside a cascade layer. The whole point of layers is that layer order beats selector specificity: a rule in cascivo.component never fights a rule in cascivo.theme on specificity — the layer order decides.

Unlayered author CSS beats every layered rule, regardless of specificity. A third-party stylesheet is unlayered author CSS. So the moment you load some-widget/dist/styles.css, its .btn { … } and even its bare div { … } rules win over your carefully-layered cascivo components. Buttons restyle themselves, spacing collapses, your theme stops applying.

The recipe

Assign the vendor CSS to a layer you declare, ordered below the cascivo layers so it can never beat them. This is native CSS — no PostCSS, no build plugin, no wrapper component.

/* app.css — loaded before anything else */

/* 1. Declare the order. `vendor` is first = lowest priority. */
@layer vendor, cascivo.reset, cascivo.base, cascivo.tokens, cascivo.component, cascivo.platform,
  cascivo.theme, cascivo.blocks, cascivo.override;

/* 2. Import the vendor stylesheet INTO the vendor layer. */
@import url('some-widget/dist/styles.css') layer(vendor);

That's it. The layer(vendor) keyword on @import drops the entire imported stylesheet into the vendor layer, which you ordered dead last, so every cascivo layer — and your own app CSS — now wins over it. You can still opt individual overrides back in with cascivo.override.

The scaffold from cascivo create already includes the vendor slot in its layer statement (only the example @import is commented), so new apps only need step 2.

The one caveat: CSS imported from JavaScript can't be layered

@import … layer() only works from CSS. If you import a stylesheet from JavaScript:

import 'some-widget/dist/styles.css' // ⚠️ lands unlayered — beats cascivo

…there is no place to attach layer(), so the CSS loads unlayered and wins. You have two fixes:

Option A — route it through a CSS file (no tooling):

import './vendor.css' // vendor.css contains: @import url('some-widget/...') layer(vendor);

Option B — @cascivo/vite-plugin (keep the JS import as-is). The plugin wraps the configured node_modules stylesheet in @layer at build time:

// vite.config.ts
import { cascivoLayers } from '@cascivo/vite-plugin'

export default defineConfig({
  plugins: [cascivoLayers({ imports: { 'some-widget/dist/styles.css': 'vendor' } })],
})

Your app CSS must still declare the vendor layer first (the two-line order statement above). The plugin is Vite-only and, like the native recipe, can't fix runtime-injected <style> tags or inline style="" — see below.

cascivo audit flags bare *.css imports from node_modules (vendor-css-import) for exactly this reason.

What the recipe can't fix

Some libraries don't ship a stylesheet at all — they inject <style> tags at runtime or set inline style="" attributes on their own elements. Those bypass the cascade layer system entirely (runtime-injected <style> is unlayered; inline styles beat all layers by construction). The only levers there are the library's own theming API and, for structural isolation, a Shadow DOM boundary. Reach for that only when a real integration demands it — the layer(vendor) recipe covers the common case of a library that ships a global .css file.

See also

As Markdown: /docs/third-party-css.md

← All guides