Skip to content
Vitops
GuidesComponentsPackagesThemeReferenceChangelog

Start here

VitopsInstallationYour design systemYour config

Live preview

Theme previewAnimation libraryIconsPatterns

Components

OverviewCSS classesWeb componentsAstro componentsBricks elements

Packages

@getvitops/cli@getvitops/astro@getvitops/vite@getvitops/generator@getvitops/core@getvitops/utils@getvitops/emdash@getvitops/create

Reference

Config referenceOutput formatsColour systemType & space scalesComponent patternsIconsConsent gateConversion trackingSearchLegal documentsCSS class vocabularyBricks elements

Releases

Changelog

Component patterns

How pattern CSS is assembled: the token cascade (defaults → groups → per-pattern overrides), per-pattern override-hook variables, state shortcuts, and role variants.

Generated page. Rendered from this project’sdesign-system.json by @getvitops/generator — the same bundlevitops docs prints against your own config. Don’t edit it by hand.

1 — Token cascade2 — Base declarations & override hooks3 — States4 — Role variants

Patterns (currently btn, cta, link, badge, card, tag, status, tooltip, dialog, popover, dropdown, notification, lightbox, comment, tabs, drawer, carousel, gallery, nav, banner, details, table, list, tree, pull-quote, combobox, forms) are authored declaratively under patterns in design-system.json (see authoring.md) and compiled to CSS in a components cascade layer — so utility classes, which live in a later layer, always win over pattern styling without specificity fights. class="card bg-danger-muted" tints the card.

The layer names differ by format, because each defers to its host:

format layer stack (last wins)
css, bricks vitops.base → vitops.components → vitops.utilities
tailwind Tailwind’s own theme → base → components → utilities

Your own CSS beats all of it. Unlayered styles outrank every cascade layer regardless of specificity, so a plain stylesheet, an Astro scoped <style>, or a Bricks-authored class overrides the framework with no !important and no specificity escalation. That is the intended override story.

The one thing to watch: a reset must be layered too, and ordered below the framework — left unlayered it beats the very component rules it is meant to sit under (a bare p { margin: 0 } would defeat .rhythm). Declare the order before you load the stylesheet, since layer precedence is fixed by first declaration:

<style>@layer my.reset, vitops.base, vitops.components, vitops.utilities;</style>
<link rel="stylesheet" href="/styles.css">

Put it after the link and my.reset becomes a new name introduced later — which sorts last, i.e. highest priority, and the reset wins. That is the one non-obvious step.

1 — Token cascade

Shared geometry resolves through a variable chain, most-specific first:

  • patterns.defaults → --<prop>-default (cascade-wide fallbacks).
  • patterns.groups.<group> → --<prop>-<group> (e.g. groups label, control, panel, area, content, pull); a pattern opts in via its group key.
  • Per-pattern: each grouped pattern gets aliases --<prop>-<name>-group → var(--<prop>-<group>), and overrides replace individual aliases with literal values.
  • patterns.radii → --br-<name> shape primitives; patterns.z → --z-tier-<name>.

2 — Base declarations & override hooks

Each pattern’s base block is emitted on its selector:

  • class only → .<class ?? name>, at normal class specificity.
  • element only → a zero-specificity :where(<element>) rule, so author CSS can always override it.
  • both → one :where(<element>, .<class>) rule (e.g. :where(button, .btn)). The element gets the styling with no class needed, the class carries it to any other tag, and — because the whole thing sits at zero specificity — any explicit class wins, including a louder pattern (.cta) or a component’s own rule (.dialog__close).

Geometry properties are wrapped in a per-pattern override hook:

base property hook variable
padding --p-<pattern>
border-radius --br-<pattern>
border --b-<pattern>
box-shadow --ds-<pattern>
font-size --fs-<pattern>
background --bg-<pattern>
background-color --bg-<pattern>

e.g. padding: var(--p-btn, 0.4em 0.8em) — a consumer restyles every button by setting --p-btn on :root, without touching the pattern. The hook is named after the pattern’s key, not its class.

background and background-color share the bg hook, since a pattern may author either. That is what makes a pattern’s fill undoable — a flat, border-only card:

<!-- via the hook: applies wherever you set it, including :root for all cards -->
<div class="card" style="--bg-card: transparent; --ds-card: none">…</div>

<!-- or compose a utility, which reads better inline and is what most authors want -->
<div class="card bg-transparent" style="--ds-card: none">…</div>

bg-transparent and bg-inherit are emitted for css/bricks; in tailwind they come from Tailwind itself. There is no utility for the shadow, so --ds-card: none is the way to drop it in every format.

Role variants are emitted as their own rules and are not wrapped, so setting --bg-card tunes the default fill without silently defeating .card-danger.

3 — States

states (hover / active / focus-visible) compile from shortcuts:

  • step: n — intensify the pattern’s colour one rung: fills swap --color-bg-<role>-solid → -solid-bold; text patterns swap --color-text-<role> → --color-text-<role>-bold.
  • scale: 0.97 — transform scale; lift: "<length>" — translate: 0 calc(-1 * <length>).
  • shadow: "<name>" — filter: drop-shadow(var(--shadow-<name>)); shadow: true — the generic lift box-shadow.
  • ring: true — focus ring (box-shadow in the role’s solid tone, or --color-border-focus when the pattern has no role; outline removed).
  • css: { … } — raw declarations escape hatch.

Any pattern with states also gets a composed transition block (translate / scale / filter / box-shadow / colours, --interact-duration / --interact-easing overridable). Hover rules are wrapped in @media (hover: hover) so touch devices never stick.

4 — Role variants

roles lists semantic colour variants: class patterns emit .<pattern>-<role> (.badge-success). A pattern with an element emits both the bare role class on the element and the -<role> form (:where(button, .btn).danger, .btn-danger), so the same variant works on a non-element host — both at class specificity, so neither outranks a plain class. Fill patterns set background-color: var(--color-bg-<role>-solid) + color: var(--color-text-on-<role>); text patterns use var(--color-text-<role>), the token guaranteed legible over a surface in both appearances. default_role colours the bare, unsuffixed pattern. States re-apply per variant with the variant’s role.

Built with the design system it documents.

@getvitops on npm