Exclusivo
Variant Tree <mv-variant-tree>
Um explorador de variantes com ramificações para qualquer elemento controlado por propriedades personalizadas de CSS (ou qualquer estado via callbacks): “Generate 4 variants” faz brotar filhos mutados da variante aplicada (um slider Spread define o quanto as mutações são ousadas), exibidos como uma árvore organizada de miniaturas ao vivo, clones inertes e em escala do alvo, então funciona com qualquer componente sem capturas de tela.
Como funciona
Um explorador de variantes em árvore para qualquer elemento controlado por propriedades CSS personalizadas (ou qualquer estado por meio de callbacks): “Generate 4 variants” gera filhos mutados da variante aplicada (um slider Spread define a ousadia das mutações), exibidos como uma árvore organizada de miniaturas ao vivo: clones inertes e reduzidos do alvo, então funciona com qualquer componente sem capturas de tela. Guarde as favoritas, descarte os becos sem saída em um resumo “3 variants discarded”, fixe até quatro variantes em uma faixa Compare em tamanho real com diferenças rotuladas (“hue 212 → 18”), cruze duas delas parâmetro a parâmetro, e passe para qualquer nó: o alvo faz a transição até ele. Árvore horizontal em contêineres largos, estrutura recuada nos estreitos, com seed, serializável, exportação de CSS em um clique.
| Categoria | Exibição de dados |
|---|---|
| Tipo | Web Component (<mv-variant-tree>) |
| Status | estável |
| Keywords | exclusive, variants, exploration, branching, tree, generative, design-tool, playground, compare, crossover, mutation, css-variables, theming, history, curation |
When to use
- Designers explore variations of a component by mutating CSS custom properties and branching from favorites
- Several variants must be compared side by side at real size with labeled parameter diffs
- An exploration must be reproducible from a seed and exported as CSS
Avoid when
- Users only need to pick one color value → use Color Picker instead
- End users of a consumer product configure a setting; this is a design and exploration tool
- The need is to record and undo a user's form edits, not to generate and explore variants → use Rewind instead
Instalação
node scripts/add.mjs variant-tree --out ./src/marvelousAgente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["variant-tree"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Arquivos copiados (dependências incluídas): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/i18n.js, core/motion.js, core/observe.js, components/variant-tree/variant-tree.js, components/variant-tree/variant-tree.css.
Uso
Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis CSS:
<div id="vt-demo" style="width:min(100%,72rem);margin-inline:auto">
<style>
#vt-demo .vt-layout { display:grid; gap:1.25rem; align-items:start }
@media (min-width:60rem) { #vt-demo .vt-layout { grid-template-columns:minmax(19rem,22rem) minmax(0,1fr) } #vt-demo .vt-stage { position:sticky; top:1rem } }
#vt-demo .vt-stage { display:grid; gap:.75rem; justify-items:center; padding:1.5rem 1rem 1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl);
background:radial-gradient(circle at 1px 1px, color-mix(in oklab, var(--mv-fg) 10%, transparent) 1px, transparent 1.5px) 0 0 / 18px 18px, var(--mv-bg-subtle) }
#vt-demo .vt-stage-label { justify-self:stretch; display:flex; justify-content:space-between; gap:1rem; color:var(--mv-fg-muted); font-size:.75rem }
#vt-demo .vt-stage-label strong { color:var(--mv-fg); font-weight:600 }
#vt-demo .vt-hint { margin:.5rem 0 0; color:var(--mv-fg-muted); font-size:.75rem; text-align:center; line-height:1.9 }
/* The explored target: a promo card driven entirely by four custom properties. */
#vt-demo .vt-promo {
--hue: 262; --radius: 18px; --scale: 1; --weight: 650;
--mv-accent: light-dark(oklch(0.56 var(--mv-accent-c) var(--hue)), oklch(0.68 var(--mv-accent-c) var(--hue)));
--mv-accent-hover: color-mix(in oklch, var(--mv-accent), var(--mv-fg) 12%);
--mv-accent-active: color-mix(in oklch, var(--mv-accent), var(--mv-fg) 22%);
--mv-accent-subtle: color-mix(in oklch, var(--mv-accent) 14%, transparent);
--mv-accent-fg: light-dark(oklch(0.45 var(--mv-accent-c) var(--hue)), oklch(0.82 calc(var(--mv-accent-c) * .8) var(--hue)));
--mv-button-radius: calc(var(--radius) * .6);
--mv-badge-radius: calc(var(--radius) * .5);
box-sizing:border-box; width:17rem; display:grid; gap:.625rem; padding:.875rem; border-radius:var(--radius);
background:linear-gradient(170deg, color-mix(in oklch, var(--mv-accent) 9%, var(--mv-surface-raised)), var(--mv-surface-raised) 55%);
border:1px solid color-mix(in oklch, var(--mv-accent) 24%, var(--mv-border)); box-shadow:var(--mv-shadow-lg);
color:var(--mv-fg); font-family:var(--mv-font-sans); text-align:left;
}
#vt-demo .vt-visual { position:relative; display:grid; place-items:center; height:7.25rem; border-radius:calc(var(--radius) * .72); overflow:hidden; color:var(--mv-fg-on-accent);
background:radial-gradient(circle at 28% 22%, color-mix(in oklch, var(--mv-accent) 40%, var(--mv-fg-on-accent)), transparent 58%), linear-gradient(150deg, var(--mv-accent), color-mix(in oklch, var(--mv-accent), var(--mv-gray-950) 42%)) }
#vt-demo .vt-visual svg { width:4.25rem; height:4.25rem; filter:drop-shadow(0 6px 10px color-mix(in oklch, var(--mv-gray-950) 35%, transparent)) }
#vt-demo .vt-visual .mv-badge { position:absolute; top:.625rem; left:.625rem; box-shadow:0 0 0 2px color-mix(in oklch, var(--mv-fg-on-accent) 35%, transparent) }
#vt-demo .vt-kicker { margin:.125rem 0 0; color:var(--mv-accent-fg); font-size:.6875rem; font-weight:600; letter-spacing:.08em; text-transform:uppercase }
#vt-demo .vt-title { margin:0; font-size:calc(1.25rem * var(--scale)); font-weight:var(--weight); line-height:1.15; letter-spacing:-.015em }
#vt-demo .vt-text { margin:0; color:var(--mv-fg-muted); font-size:.8125rem; line-height:1.45 }
#vt-demo .vt-price { display:flex; align-items:baseline; gap:.5rem; margin:.125rem 0 .125rem }
#vt-demo .vt-price strong { font-size:calc(1.5rem * var(--scale)); font-weight:var(--weight); letter-spacing:-.02em; font-variant-numeric:tabular-nums }
#vt-demo .vt-price s { color:var(--mv-fg-subtle); font-size:.875rem }
#vt-demo .vt-price span { margin-inline-start:auto; color:var(--mv-success); font-size:.75rem; font-weight:500 }
#vt-demo .vt-promo .mv-button { width:100% }
</style>
<div class="vt-layout">
<div class="vt-stage">
<div class="vt-stage-label"><strong>Live preview</strong><span>Follows the applied variant</span></div>
<article class="vt-promo" id="vt-promo" data-vary="--hue: 0..360; --radius: 4..28px; --scale: .9..1.25 as type scale; --weight: 500|650|800">
<div class="vt-visual">
<span class="mv-badge" data-appearance="solid" data-variant="danger">−30%</span>
<svg viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" aria-hidden="true"><path d="M12 40v-8a20 20 0 0 1 40 0v8"/><rect x="8" y="38" width="12" height="18" rx="5" fill="currentColor" stroke="none"/><rect x="44" y="38" width="12" height="18" rx="5" fill="currentColor" stroke="none"/></svg>
</div>
<p class="vt-kicker">Back-to-school deal</p>
<h4 class="vt-title">Aurelo ANC Headphones</h4>
<p class="vt-text">Active noise cancelling, 40-hour battery, folds flat for travel.</p>
<p class="vt-price"><strong>$139</strong><s>$199</s><span>Free shipping</span></p>
<button class="mv-button" type="button">Get the deal</button>
</article>
</div>
<div>
<mv-variant-tree id="vt-tree" for="vt-promo" count="4" seed="180" spread="0.45"></mv-variant-tree>
<p class="vt-hint">Click a thumbnail to apply it · double-click to branch from it · in the tree:
<kbd class="mv-kbd">↑</kbd> <kbd class="mv-kbd">↓</kbd> browse, <kbd class="mv-kbd">F</kbd> keep, <kbd class="mv-kbd">P</kbd> compare, <kbd class="mv-kbd">G</kbd> generate, <kbd class="mv-kbd">Del</kbd> discard</p>
</div>
</div>
<script type="module">
const vt = document.getElementById("vt-tree");
await customElements.whenDefined("mv-variant-tree");
// A past exploration session, so the tree tells a story on first sight:
// four first tries, three dead ends discarded, one branch explored further.
const quiet = { animate: false };
const [a, b, c, d] = vt.generate(4, quiet);
[a, c, d].forEach((id) => vt.prune(id));
const kids = vt.generate(4, { ...quiet, from: b });
vt.favorite(kids[1]);
vt.pin(kids[1]);
vt.pin(kids[3]);
vt.checkout(kids[1], quiet);
vt.addEventListener("mv-checkout", (e) => console.info("mv-checkout", e.detail.id, e.detail.params));
</script>
</div>API
Attributes
| Name | Tipo | Padrão | Descrição |
|---|---|---|---|
for | string | Id of the element to explore (looked up in the same root; waited for if rendered later). Without it, the first child element is the target. | |
params | string | Explorable parameters, same syntax as data-vary on the target (which is used when this attribute is absent). | |
data-vary | string | On the target: “--hue: 0..360; --radius: 0..28px step 2 as corners; --weight: 400|600|800”. Numeric ranges (unit taken from the bounds, optional step, wrap for circular values; a 0..360 hue wraps by default) or discrete sets separated by |. “as <label>” names the parameter in the UI; otherwise the last segment of the property name is used. Plain CSS properties (opacity…) work too. | |
count | number | 4 | Children created per generation (1-12). |
spread | number (0-1) | 0.4 | Mutation strength, mirrored by the Spread slider: subtle nudges and neighbor values when low, far jumps when high. At least one parameter always changes, siblings never duplicate each other. |
seed | number | Seeds the random generator: the same seed and actions give the same tree (demos, tests, shareable explorations). | |
label | string | Explore variants | Panel title. |
max-previews | number | 16 | Maximum live clones in the tree. Priority: applied path, pinned, kept, then newest; the others show a cheap bar glyph of their parameters. |
layout | auto | horizontal | vertical | auto | Tidy horizontal tree, or indented outline. auto switches to the outline under 520px of component width (ResizeObserver). |
Properties
| Name | Tipo | Descrição |
|---|---|---|
params | Param[] | string | Parsed parameters ({ name, label, type: "range", min, max, unit, step, wrap } | { name, label, type: "set", values }). Assign an array or a string to redefine them (restarts the tree). |
nodes | { id, parent, mate, label, params, favorite, pinned, pruned, current }[] | Serializable tree. Assign a saved array to restore an exploration (can be set before the element is defined). |
current | { id, params, css } | The applied variant, with its CSS declarations ready to paste (read-only). |
mutate | (state, rng, spread) => state | Custom mutation for arbitrary state (return a new object or mutate the copy). rng() is the seeded generator. |
apply | (state, el) => void | Custom rendering: called on the target and on every preview clone. Replaces the default custom-property writes (no tween). |
strings | Partial<Record<string, string | { one, other }>> | Overrides for the default text (en-US): original, variant, variantShort ({n}, node names), theOriginal, theVariant ({n}, inside sentences), copy, copied, copyFailed (copy button), generate (plural { one, other }, {n}), spread, spreadLabel, spreadValue ({word}, {percent}), subtle, moderate, bold, keep, compare, discard, combine, actions (toolbar label), help (keyboard help), tree, noTarget, variantA, variantB, createHybrid, cancel, start, generation ({n}, column heads), metaApplied ({name}), metaVariants (plural, {n}), metaKept, metaDiscarded ({n}), hybridOf ({a}, {b}), kept, inComparison ({n}), collapsed (plural, {n}), stubDiscarded (plural, {n}), stubLabel ({label}), showAgain, compareHint ({name}), appliedVariant, identical, becomes, apply, applyLabel, removeLabel ({name}), remove, pickFrom ({label}, {value}, {name}), identicalList ({list}), combineNone, combineDuplicate ({name}), combineHybrid ({a}, {b}), announceGenerated (plural, {n}, {name}), announceApplied ({name}, {summary}), announceKept, announceUnkept, announcePinned, announceUnpinned ({name}), announcePinnedReplacing ({name}, {dropped}), announceSameHybrid ({name}), announceCrossed ({name}, {a}, {b}), announceDiscarded ({name}), announceDiscardedTree (plural, {n}, {name}), announceRestored (plural, {n}), announceNeedTwo, announceKeepOriginal, announceCopied ({css}), announceCopyFailed. Parameter names and numbers stay as written (en-US numbers). App-wide translations: setStrings() from core/i18n.js, under the key "variant-tree". Can be set before the element is defined. |
Methods
| Name | Descrição |
|---|---|
generate(n?, { from?, animate? }) | Creates n mutated children of the applied node (or from) and returns their ids. animate: false skips the sprout animation (seeding). |
checkout(id, { animate? }) | Applies a node to the target; numeric parameters tween (hue the short way around), discrete ones switch. |
favorite(id?, force?) | Toggles the star (defaults to the applied node). |
pin(id?, force?) | Toggles a node in the Compare strip (4 at most, the oldest makes room). |
combine(a, b, picks?) | Creates and applies a hybrid child of a: for each differing parameter, the value of a or b (picks: { name: "a" | "b" }, random but mixed when omitted). Returns its id, or null if it would equal a parent. |
prune(id?) / restore(id) | Folds a node and its descendants into a stub / unfolds a node or every pruned child of id. |
reset(state?) | Starts over from state (default: the target's current computed values). |
refresh() | Re-clones the previews (content changes inside the target are also picked up automatically). |
Events
| Name | Descrição |
|---|---|
mv-generate | New children. detail: { parent, ids, params[] }. |
mv-checkout | A node was applied. detail: { id, params }. |
mv-combine | A hybrid was created. detail: { id, a, b, params }. |
mv-favorite | detail: { id, favorite }. |
mv-prune | detail: { id, count }. |
Content structure
| Name | Descrição |
|---|---|
(content) | Optional: the target itself, when for is not used. |
CSS classes
| Name | Descrição |
|---|---|
mv-variant-tree-panel / -head / -bar | Generated panel, header (title, status, Copy variables) and toolbar acting on the applied variant. |
mv-variant-tree-view / -node / -thumb / -stub / -edge | Scrollable tree canvas, tiles (role treeitem) with their live thumbnail, discarded-variants stub and SVG edges (data-path on the applied path, data-kind="mate" for a hybrid's second parent). |
mv-variant-tree-combine / -compare / -chip | Crossover panel, Compare strip and diff chips. |
CSS variables
| Name | Padrão | Descrição |
|---|---|---|
--mv-variant-tree-accent | var(--mv-accent) | Applied node, path and primary actions. |
--mv-variant-tree-favorite | var(--mv-warning) | Star of kept variants. |
--mv-variant-tree-mate | var(--mv-info) | Side B of a crossover and the hybrid's second edge. |
--mv-variant-tree-thumb-bg | var(--mv-bg-subtle) | Background behind the clones. |
--mv-variant-tree-bg | var(--mv-surface) | Panel background. |
--mv-variant-tree-max-height | 42rem | Height of the tree view before it scrolls. |
Accessibility
The tree is a real role="tree": each tile is a treeitem with aria-level, aria-setsize, aria-posinset, aria-expanded and aria-selected (the applied variant), named by its parameter summary (“Variant 3 · hue 18, radius 12px · kept · in comparison, slot 1”). Roving tabindex (Tab enters the tree on the applied variant): Up/Down/Home/End browse in reading order, Right/Left expand, collapse or move to child/parent, Enter or Space applies, F keeps, P compares, G generates from the focused node, Delete discards (focus moves to the stub; Enter on it restores and focuses the first variant back). Thumbnails are aria-hidden and inert clones (ids, names and labels stripped, never focusable). Applying, generating, keeping, comparing, combining and copying are announced in a polite live region; the Spread slider has an aria-valuetext (“Moderate, 45%”); unavailable actions stay focusable with aria-disabled and explain why when used. Diff chips read “hue 212 becomes 18”. Focus rings everywhere. Reduced motion (OS or data-motion="reduce"): no sprout, bloom, edge growth or glide, and the target switches instantly instead of tweening. Forced colors: the applied node, its path, pressed Keep / Compare and the Spread slider switch to system colors and outlines. Known limits: preview clones lose their ids, names and ARIA references, so style the target by class rather than by id; custom elements inside the target run in every live thumbnail (capped by max-previews); a custom apply switches without tween; texts and announcements follow the strings property (en-US by default), while numbers stay en-US with no locale override yet.