The Design Layer

This page is a design-ownership decision guide. Its CSS and component snippets are partial patterns that assume an existing Wippy frontend package and build.

A Wippy frontend can contain many independently published modules in one application. The theme reaches every surface, while each module owns its local presentation. A shared design layer covers the narrower case where several modules share a concept that the theme does not provide.

The layers

Layer Reaches Owns
Theme Every surface, including modules you do not own PrimeVue components, the shared semantic tokens, documented classes
Shared design layer Only the modules that opt in Vocabulary those modules share that has no themed component behind it
Module Itself What is genuinely specific to one surface

The theme is universal, and that is the constraint

The theme styles markup you do not own. Any module — including a third-party plugin written by someone who has never seen your app — renders into the same host and is painted by the same theme. That is what makes the theme the universal layer, and it cuts both ways:

Nothing app-specific may go into the theme, because it would be imposed on every module that never asked for it.

A module may not depend on anything app-specific being in the theme. The contract is PrimeVue components + the shared Wippy semantic tokens + documented classes — nothing an application added on top. Note that PrimeVue's own presets are not the contract either: Wippy runs PrimeVue with theme: 'none', so it is the Wippy semantic tokens you rely on.

/* GOOD — shared Wippy semantic tokens, present for every module */
.my-panel {
  color: var(--p-text-color);
  background: var(--p-content-background);
  border: 1px solid var(--p-content-border-color);
}

/* BAD — an application-specific token. Your module now only works inside
   one app, and silently loses the declaration anywhere else: an undefined
   custom property makes the declaration invalid at computed-value time, so
   it drops and the element quietly inherits instead. */
.my-panel { background: var(--kx-surface-2); }

This is also the answer to "can I put our shared vocabulary in the facade?" Only if it must genuinely reach arbitrary, unowned markup. If it is scoped to your set of modules, it does not belong in the theme — it belongs in the layer below.

The backbone, and when a component may opt out

PrimeVue and Tailwind, as shipped by the host, are the recommended backbone for any component. A component may opt out — but the opt-out narrows the moment it renders anything conventional, and the ladder only goes one way:

The component… Then it must load
is presentation-neutral — canvas, SVG, a chart with no controls, no tokens, no utilities, no scrolling nothing: hostCssKeys: []
consumes semantic tokens or dark mode themeConfigUrl
can scroll iframeCssUrl
renders markdown markdownCssUrl
chooses Tailwind utilities for routine layout or spacing primeVueCssUrl (the Host bundles Tailwind with this asset)
renders anything PrimeVue ships a component for — button, input, form, table, dialog, menu, tag, tooltip, any feedback control primeVueCssUrl and PrimeVuePlugin

A chart on a canvas is the archetypal legitimate opt-out: it has no classic UI, so it needs none of the backbone. Give that same chart a toolbar and it is no longer presentation-neutral — the button is a PrimeVue button, and the whole integration comes with it.

Note the coupling: Tailwind utilities are delivered with primeVueCssUrl. There is no separate Tailwind host CSS key, so in practice a component that chooses Tailwind is loading the PrimeVue asset too. Prefer utilities for ordinary layout and spacing when they keep the component clear, but portable module-owned CSS remains valid when a utility is not the best expression of the design. (preflightCssUrl is not part of the key union; if Tailwind preflight is genuinely required inside the shadow root, load it imperatively — rarely needed.)

The practical consequence for this page: most of what a module wants already exists in the backbone. The shared design layer is a narrow band above it, not a place to re-do what PrimeVue and Tailwind already cover. See CSS Injection for the mechanics.

The shared design layer

Some ideas recur across a known set of modules and have no application-level contract in the theme: a domain-specific match summary, a surface header row, an empty state, or a project-specific tag-sizing vocabulary. These concepts belong in the shared design layer.

They ship as a published package, materialized into each consumer at build time. It must be a package rather than a path alias because consumers live in different repositories. A module in another repository, with no path access to the producer, must be able to consume the vocabulary and build.

The producing module declares the package as a build-time artifact and each consumer materializes it into its own tree. See Build-time Artifacts for the declaration, the node-package format, what the runtime reconciles for you, and the glue a build still has to supply itself.

The module

Everything else, plus every deliberate divergence from the shared vocabulary.

Deciding where something belongs

Ask in order. First yes wins.

  1. Is it a value? Colour, radius, spacing, elevation, severity. → Theme. Read a semantic token. Never a literal.
  2. Does the theme already ship a component for this? Button, Dialog, Select, Tag. → Theme. Use the component. Style it by putting a class on it — never rebuild it.
  3. Do two or more of your modules need this same concept, with no themed component behind it? → Shared design layer.
  4. Otherwise → Module.

Worked examples

The examples use the kx- prefix for application-specific classes and stylesheet names. The placement rules apply to any Wippy application.

Never rebuild a themed component

PrimeVue ships Button. Replacing it with .kx-btn on a native <button> creates a second implementation whose interaction and appearance can drift from the themed component.

Bad: a native button element carrying .kx-btn .kx-btn-primary — a second implementation of a component the theme already ships.

Good: the themed component, with a class on it when you need to adjust it.

<Button label="Save" class="kx-save" />

When the themed component does not fit, that is not a licence to rebuild it. Put a class on the component and style that class — in the facade if the adjustment is app-wide, in the module if it is local.

Severity is the theme's, not yours

Severity — success, danger, warn, info — is theme semantics with published ramps. Re-deriving it under module-local names creates competing definitions that can diverge across modules.

/* BAD — severity re-derived under a module-local name */
.tone-gn { color: #16a34a; }

/* GOOD — severity from the theme */
.status-dot.success { background: var(--p-success-500); }

A tone may still exist in the shared layer — but only as decorative category colour, never as severity. If it can mean "this failed", it is severity and it is the theme's.

Shared vocabulary the theme has no seat for

/* GOOD — this application-specific card contract and empty-state vocabulary
   recur across modules. PrimeVue's generic Card does not define these domain
   semantics, so the shared layer owns them. */
@import "@kickside/ui-kit/kx-card.css";
@import "@kickside/ui-kit/kx-state.css";

Adopting means import and delete

A CSS @import must precede every other rule in a sheet. The shared sheet therefore always lands first, and anything the module declares afterwards beats it at equal specificity. A module that imports the package and keeps its own copy has changed nothing at all.

/* BAD — the import is inert; the local copy still wins */
@import "@kickside/ui-kit/kx-card.css";
.kx-card { border-radius: 14px; border: 1px solid var(--p-content-border-color); }

/* GOOD — import, delete the local copy, keep only a documented delta */
@import "@kickside/ui-kit/kx-card.css";
/* This surface's cards are inline in a dense list, so they lose the lift. */
.kx-card:hover { transform: none; }

Keep the delta only — never restate the whole body. And never fold two intents into one name: if a class name means different things in two modules, that is two concepts wearing one name. Split the name; do not pick a winner and repaint the loser.

Specificity against the theme

The module's CSS is injected into the shadow root first; the theme's PrimeVue sheet is appended afterwards. Both are <style> elements, so document order decides and the theme is second. A module rule that must beat a themed component class needs more specificity — not a later line in the file. (adoptedStyleSheets carries the facade's custom CSS, not the theme, so reaching for an adopted sheet does not win this either.)

This is most visible with pass-through classes, where your class lands on a themed element:

/* BAD — this class is applied to PrimeVue's own footer element, so at equal
   specificity the theme wins and the padding never applies. */
.kx-modal-foot { padding: 14px 18px; }

/* GOOD — scoped under the dialog root, so it out-specifies the theme */
.kx-modal > .kx-modal-foot { padding: 14px 18px; }

What the shared layer may contain

Everything a set of modules genuinely shares and the theme does not own: CSS vocabulary, derived tokens, internal components, helpers, and test harnesses.

Use semantic chunks. Each unit should be one named concept a consumer can reason about — kx-card, kx-state, kx-tag. Prefer finer-grained packages so a consumer takes only what it needs; a single package shipping several clearly-named units is workable, but it is not the shape to aim for.

Use specific names. Avoid catch-all units such as common, shared, misc, or utils. A unit whose name does not describe its contents will accumulate unrelated concepts and recreate the duplication this layer is meant to remove.

Normalising is a visual change

Consolidating drifted copies can change rendering. Compare every definition, choose the canonical version, record the reason, keep deliberate divergence as a documented override, and inspect the result visually. Unit tests cannot see layout.