Paper is the family of design systems, kits, and compat packages built on
@vuetify/v0, published under the @paper/* scope. Each package stands on its
own — v0 is the only substrate.
This document is the family contract. Per-package specifics live in that package's
SPEC.md (see Per-package SPEC.md); this document defines what
is common, what is required, and what is forbidden — so patterns are decided once, not
rediscovered per component.
Rulings 1–3 and 6–8 are decided; sections 4–5 are PROPOSED and open for review.
The stack:
@vuetify/v0 the substrate — compound components, composables, utilities,
and the theme engine (incl. V0StyleSheetThemeAdapter)
@paper/* design systems, kits, and compat (the classes below),
each standalone on v0
vuetify orchestration + defaults. Wraps a design system
(v0 Button → MdButton → VBtn). Does not own Material.
Not every @paper/* package is a rich design system. Genesis is a kit; Bulma is
compat. Most design systems will be feature-rich because they implement a full
specification. A kit missing a token pipeline is correct, not incomplete — do not
pad Genesis to look like Emerald.
Original design systems ship in this order. Compat work does not wait on it.
| Package | Class | Status | Role |
|---|---|---|---|
@paper/emerald (Em*) |
design system | shipping (preview) | First commercial DS. Figma Emerald 1.0. Showcase for v0. |
@paper/onyx (On*) |
design system | next | Second original language. Proves Paper is a family, not Vuetify's CSS extracted. Paper primitives (useColor / useContrast) and shared icon extraction are gated here. |
@paper/material (Md*) |
design system | after Onyx | Complete pinned MD3. Vuetify consumes it (VBtn wraps MdButton). Ships after Onyx so Material is one language among several, not Paper's definition. |
@paper/genesis (Gn*) |
kit | shipping | Docs primitives. Purpose-scoped, --v0-* only. |
@paper/bulma (Bu*) |
compat | shipping | Bulma markup, v0 JS. |
@paper/bootstrap (B*) |
compat | parallel | Prefix reserved. Search wedge; may ship alongside Onyx / Material. |
| daisyUI | compat | external (daisyfy) | Named; not in-repo. |
The @paper/* scope hosts three deliberately different kinds of package. Identify the
class first — a kit missing a token pipeline is correct, not incomplete; a design
system missing one is broken; a compat package that owns a token prefix is in the
wrong class.
| Design system | Kit | Compat | |
|---|---|---|---|
| Intent | Complete, exclusive implementation of one specification | Purpose-scoped component set | Upstream CSS framework's markup, v0's JavaScript |
| Exemplars | Emerald, Onyx, Material | Genesis (docs) | Bulma (@paper/bulma); daisyUI (daisyfy, external); Bootstrap (B*, reserved) |
| Token namespace | Owns one (--emerald-*, --onyx-*, --material-*) |
None — consumes --v0-* |
None — consumes upstream's (--bulma-*) |
| Theme plugin / adapter | Required (theme.ts, adapter.ts, plugin.ts) |
Forbidden | Forbidden at Tier 1. A later plugin would drive upstream vars, never an owned prefix |
| Stylesheet artifact | dist/theme.css (prebaked default theme) |
None — inherits the page's theme | None — the user's own upstream CSS is the styling source |
| Component coverage | The pinned spec, all of it, and nothing else (ruling 7) | Only what the purpose needs | Upstream's documented component / element / form families; composites are labeled Tier 2 |
| Visual language | Its own | Blends with whatever theme is active | Upstream's, verbatim |
| Component prefix | Em*, On*, Md* |
Gn* |
Bu* (B* reserved for Bootstrap) |
| Installable without Vuetify | Required | Required | Required |
"Custom design system via bring-your-own Figma" is a delivery mode of the design system class, not a fourth class: same contract, same pipeline, tokens sourced from the customer's file. This is why the token pipeline must be reproducible end to end.
Pitch: delete the framework's JavaScript, keep the CSS. Components render the upstream
framework's real markup and classes (button is-primary, is-active) against the
user's own stylesheet; @vuetify/v0 supplies behavior, focus, and accessibility.
The class exists because the logic layer is the expensive part, and v0 already ships
it. Each compat package is a search wedge (bulma vue, daisyui vue) into v0 — not
a conversion onto a new visual language.
Declared deviations from rulings 1–3 — inherent to the class, not exceptions to file per component:
- No owned token namespace (ruling 1).
--bulma-*belongs to upstream. - Native form controls where upstream CSS demands them (ruling 2). A non-native
<select>gets none of the user's CSS. Rich replacements (combobox, etc.) live in Tier 2 on full v0 primitives. - Upstream state classes, unprefixed (ruling 3).
is-activeinstead ofdata-active. State classes are component-owned and JS-set, never CSS — a hard requirement. Docs sandboxes classify overlay / float / grow from the presence of.is-active; a future target that styles open state with:hoveronly cannot reuse that classifier.
Additional rulings, compat only:
- No named slots. Every region is an express part component named after the
upstream part class (
BuModalHead,BuDropdownMenu). Those parts also attach to the parent export (BuModal.Head,BuDropdown.Menu); the flat names remain valid. List-shaped families (breadcrumb, pagination, menu, file) are parts too, notitemsprops. Parts read parent state through optional context and warn in dev when it is missing; parts backed by a v0 context throw instead. - Optional peer for the upstream CSS package. Zero-config consumers load it off-CDN. The package never imports it.
- Conformance harness is the factory asset: fixtures captured from upstream
docs, DOM / class diff with a strict additive-attr charter (
aria-*/data-*/role/tabindex/idonly). - Isolation comes from upstream class names, not a package prefix. Ruling 3's
"never
<style scoped>" still holds.
This addendum is extracted from @paper/bulma and daisyfy. A third target
(Bootstrap) will stress-test it; until then the table is the contract.
Tokens are TypeScript objects (theme.ts), published as CSS custom properties at
runtime by a stylesheet adapter extending v0's theme engine
(V0StyleSheetThemeAdapter, constructed with the design system's prefix). Two
consumption paths, both required:
- Zero-config: the build prebakes the default theme to
dist/theme.css. A newcomer installs, imports one CSS file, and components render correctly — no plugin. - Full:
app.use(create<Name>Plugin())for theming, dark mode, runtime switching, CSP nonces.
Component styles reference tokens with literal fallbacks — var(--emerald-primary, #26C26D) — so components degrade to the default look rather than breaking when neither
path is wired. Design values live in tokens; literals appear only as var()
fallbacks or in theme.ts.
Kits skip all of this: consume --v0-* variables with literal fallbacks, exactly as
@paper/genesis documents in its SPEC.md.
A design system that hosts a kit (today: Genesis chrome on an Emerald docs page) must
still provide the --v0-* cascade the kit reads. Preferred mechanism: the design
system's stylesheet adapter also emits --v0-* aliases for the color roles kits
consume (surface, on-surface, surface-tint, on-surface-variant, primary,
on-primary, pre, background, on-background, and the severity tokens kits use for
admonitions). Kits stay prefix-blind; the DS owns the bridge.
/* Emitted alongside --emerald-* by the DS adapter (illustrative) */
[data-theme="emerald-light"] {
--emerald-primary: #26c26d;
--v0-primary: var(--emerald-primary);
/* …same for surface / on-surface / … */
}Alternatives (register a parallel v0 theme; host-only alias stylesheet) are allowed. Do not teach kits a DS prefix — that reintroduces the dual-cascade failure mode Genesis already rejected (Phase 1 → revised).
- Behavioral components compose v0 compound primitives directly —
Button.Root,Dialog.Root,Tabs.Root, and friends own behavior, accessibility, and state. - Purely presentational components (cards, badges, dividers) render semantic
elements or v0's
Atom; there is no behavioral primitive to compose and none should be invented. - Styling logic comes from v0's utilities (
apca,foreground,hexToRgb, …) and the package's own tokens; no published@paper/*package routes styling through an intermediate styling-primitives layer. (@vuetify/paperis removed from the version/publish path and kept dormant in-repo. Whether v0's utilities fully cover the color/contrast math a design system needs — and so whether Paper's primitives are still required — is to be evaluated against the second design system, Onyx: decision deferred, not made.) - Never compose native HTML form controls.
- Never
<style scoped>. Scoped rules silently fail on multi-root compound children (thedata-vattribute never lands on the primitive's root — this bit Genesis in #359). One rule, zero judgment calls. - Isolation comes from the class prefix: every class is namespaced by package
(
emerald-button,genesis-docs-example). - State styling hooks are data attributes (
data-variant,data-size,data-disabled), not JS-computed class lists. - No utility classes in package source.
The config layer is mechanical and must be generated, not authored — scaffolded new packages start from the canonical shape rather than copying a sibling:
package.json: two-tier exports (developmentcondition →src/index.ts,publishConfigdist-only),@vuetify/v0as the sole internal dependency (workspace:^),vue >=3.5.0peer (components use reactive props destructure),catalog:dev deps.tsconfig.json: extends@vue/tsconfig/tsconfig.dom.json;#<name>and#v0path aliases;libincludesesnext; nobaseUrl.tsdown.config.mts:unplugin-vue/rolldown,dts: { vue: true }, the deferred__DEV__: 'process.env.NODE_ENV !== \'production\''expression (never baked at build time),exports: { devExports: 'development' }.- Design systems additionally:
src/theme.ts,src/adapter.ts,src/plugin.ts, and the build step that prebakesdist/theme.cssfrom the default theme (ruling 1).
A design system component ships only when:
- every variant and size in its spec exists (no "1 of 4 variants");
- interactive states are styled: hover,
:focus-visible, disabled, loading where applicable; - dark-mode tokens resolve wherever the design system defines a dark theme;
- its tokens are wired — a token defined in
theme.tsbut unused by any component, or a component hardcoding a value a token exists for, fails review.
Kits gate only on their purpose's spec.
Every design system ships its own icon set. There is no shared artwork and no bundled icon library — a design system's glyphs are as much a part of its visual language as its type scale, and Onyx's chevron is not Emerald's.
What is shared is the role vocabulary and the resolution mechanism:
- A design system maps role names to glyphs —
close,check,chevron-down,search,menu— and registers them through v0'screateTokens(#107). Alias resolution is the reasoncreateTokensis the right primitive:{envelope: '{mail}'}costs one entry, and overridingmailmoves both names at once. - Roles are named for what the glyph is, not the one screen that first needed it —
receipt, notorders. Two features drawing the same mark get one role and an alias, never two copies of the path data. - Icons are their own plugin, built with v0's
createPluginContextand carrying a lazy fallback so the renderer still draws with nothing installed. The design system's main plugin composes it; consumers extend or replace roles through its options. Overriding a role must restyle the system's own chrome, which means components may not inline their own artwork — anEm*/On*/Md*component that hardcodes an SVG is a component a consumer cannot rebrand. - Do not promise the glyph map is tree-shakeable. Measured against Emerald's build
(rollup and esbuild,
dist/index.mjs): a module-scope trinity —const [a, b, c] = createXContext()— survives tree-shaking even annotated/* @__PURE__ */, so the map is retained by any import from the package. Skipping the plugin install is a registration switch, not a bundle switch. A separate entry point is the only mechanism that would actually drop it, and no design system has needed one yet. - Whatever a design system's glyphs are (path data, sprite ids, icon-font classnames,
UnoCSS
i-*classes), the role layer is the same; the renderer is the design system's own component (EmIcon,OnIcon,MdIcon).
Emerald is the first implementation — packages/emerald/src/icons.ts plus EmIcon,
drawing 24x24 stroke-grid path data. Whether the role vocabulary, the collect/alias
plumbing, or the renderer itself is worth extracting into a shared package is to be
evaluated against the second implementation (Onyx): one consumer is not a pattern.
Decision deferred, not made — until then, a second design system copies the approach,
not the code.
A design system is a complete, exclusive implementation of one specification. Users always know exactly what they get. Fire-and-forget: the package is the spec.
- Complete. Every component, variant, and state in the pinned spec ships (ruling 5
is the per-component bar). If MD3 has a date picker,
@paper/materialhas one. - Exclusive. Nothing the spec does not contain, even when v0 already has the
primitive. If the spec has no sparkline, there is no
MdSparkline. A v0 primitive existing is not a reason to ship. - Pinned.
SPEC.mdnames the spec so "complete" is auditable: Figma file key + version, "MD3 as of YYYY-MM", upstreambulma@1.0.4for compat. Unversioned "we implement Material" is a treadmill; Google (and Figma files) move. - Escape hatch. A missing piece is the consumer's: compose a v0 primitive with
the package's tokens and classes (or upstream CSS, for compat). That composition
is unsupported as a Paper component. Document the hole; do not grow a silent
leftover tier. Compat Tier 2 (e.g. bulma-extensions) is the labeled exception —
extra-spec work must say so in
SPEC.md. - API shape is the spec's, not Vuetify's. MD3 filled / outlined / tonal buttons
and color roles belong on
MdButtonbecause they are Material, not becauseVBtnhasvariant/color. Vuetify density, ripple,to, and named slots (#prepend) do not.
Kits already live this rule: coverage is the purpose, not v0's catalog. Genesis
shipping no GnDialog is correct.
A design system is installable without Vuetify (pnpm add @paper/material,
MdButton, createMaterialPlugin()). An orchestrator may wrap DS components and
put its own interface on top. That is how Vuetify keeps identity — defaults,
density, router, and the non-spec long tail (VVideo, VSparkline, …) — without
Paper packages becoming Vuetify.
v0 Button.Root → MdButton (pinned MD3 API) → VBtn (Vuetify API)
- The wrapper translates. Non-spec props (
density,ripple,to) must not fall through onto the DS vnode. Mapping is explicit. - The DS does not grow orchestrator API to make the wrapper trivial.
- One stylesheet paints. Dual CSS (
VBtn.sassand Material CSS on the same node) is forbidden. - The wrappee surface is the spec API plus composition hooks (
attrs/expose/ the root element). It is not a Vuetify-shaped prop list "so the wrapper is easier." - The DS still has app authors as a first-class consumer. Wrappability is a constraint, not the product.
Every @paper/* package carries a SPEC.md declaring:
- its class (design system, kit, or compat) and purpose;
- its pinned spec — design systems: Figma file key + version, or named spec + date (e.g. MD3 as of YYYY-MM). Compat: upstream package + verified version. Kits: the purpose boundary;
- a coverage statement: what the spec contains that ships, and what is deliberately out (ruling 7). A v0 primitive in the "out" list is not a bug;
- its component inventory and any intentional deviations from this contract, with reasons.
packages/genesis/SPEC.md is the exemplar of the kit shape, and declares its class.
Genesis otherwise predates this contract (most of its components still use
<style scoped>); that alignment is tracked separately and does not weaken the rulings
above.