ExklusivBeta
Deliberation <mv-deliberation>
Gruppenentscheidungen in Runden mit verdeckten Stimmen und sichtbarer Annäherung: Schätzungen (Planning Poker), Go/No-Go-Entscheidungen, Hiring-Debriefs, Design-Kritiken, Priorisierungen oder Abstimmungen über Retro-Maßnahmen. Jeder Teilnehmer stimmt geheim auf einer Skala ab (Fibonacci, T-Shirt-Größen, Go/No-Go, Einstellung, Zuversicht, ein Zahlenbereich oder deine eigene Liste, optional mit „?“ und Abstain); der Tisch zeigt mit verdeckten Karten, wer abgestimmt hat, niemals was. Reveal dreht alle Karten gleichzeitig um, dann wird die Runde ausgewertet: Verteilung mit jedem Abstimmenden als Chip, Median, Spannweite, Übereinstimmung, der Konsenskandidat und die Abstimmenden, die weit von der Gruppe entfernt liegen, im Klartext benannt („Aiko and Mateo voted far from the group (13 pts and 1 pt): hear them first“), oder eine Spaltung in zwei Lager, wenn es sie gibt. Die Gruppe diskutiert und stimmt erneut ab; ein Konvergenzdiagramm trägt jede Runde auf derselben Skala ein, sodass man sieht, wie sich die Spannweite verengt, und sobald die Regel erfüllt ist (einstimmig, qualifizierte Mehrheit, Mehrheit oder Entscheidung des Moderators, optional mit Rundenlimit), hält der Moderator die Entscheidung mit ihrer Begründung fest. Anonyme Runden legen die Karten nach Wert aus, losgelöst von Namen, und bleiben auch danach anonym; ein optionaler Timer deckt auf, wenn er abläuft. Die Daten bleiben bei der App: Sie synchronisiert Stimmen über ihr eigenes Backend mit vote(), markVoted() (der Server hält die Werte geheim) und reveal(votes), und jeder Schritt läuft über abbrechbare Events mv-vote, mv-reveal (mit waitUntil), mv-round und mv-decision.
| Kategorie | Datenanzeige |
|---|---|
| Typ | Web Component (<mv-deliberation>) |
| Status | Beta |
| Installiert auch | button, textarea |
| Keywords | exclusive, culture, voting, vote, consensus, planning-poker, estimation, story-points, go-no-go, decision, decision-log, hiring, debrief, retro, team, collaboration, facilitation, outliers, convergence, chart, realtime, anonymous |
When to use
- A team estimates backlog items together and needs independent votes before anyone anchors on the loudest number
- A release, launch or incident review needs an explicit go/no-go where every dissent is surfaced and heard
- A hiring debrief or design critique should collect private ratings first, then discuss the outliers and re-vote
- A decision must be logged with its rule, tally and rationale once a group actually reaches consensus
Avoid when
- People each contribute something different toward one shared goal rather than voting on one question → use Stone Soup instead
- A single person rates a product or answer as form input → use Rating instead
- A one-shot poll or survey with many questions and no discussion rounds: a form or poll tool is simpler
Installation
node scripts/add.mjs deliberation --out ./src/marvelousKI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["deliberation"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Kopierte Dateien (inklusive Abhängigkeiten): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/deliberation/deliberation.js, components/deliberation/deliberation.css, components/button/button.css, components/textarea/textarea.css, components/textarea/char-count.js.
Verwendung
Referenz-Markup zum Starten und Anpassen mit Attributen, data-* und CSS-Variablen:
<div id="dl-demo" style="display:grid;gap:1.75rem;width:min(100%,68rem);margin-inline:auto">
<style>
#dl-demo .dl-bar { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:.75rem 1.5rem }
#dl-demo .dl-bar h3 { margin:0; font-size:1rem; letter-spacing:-.01em }
#dl-demo .dl-bar p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
#dl-demo .dl-controls { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem 1.25rem }
#dl-demo .dl-controls .mv-choice { font-size:.8125rem }
#dl-demo .dl-hint { margin:-.75rem 0 0; color:var(--mv-fg-muted); font-size:.75rem; text-align:center }
#dl-demo .dl-sep { height:1px; background:var(--mv-border) }
</style>
<div class="dl-bar">
<div>
<h3>Sprint 42 planning</h3>
<p>You are facilitating. Round 1 was far apart; round 2 is under way.</p>
</div>
<div class="dl-controls">
<label class="mv-choice">
<input type="checkbox" role="switch" class="mv-switch" data-size="sm" id="dl-anon">
<span class="mv-choice-text"><span class="mv-choice-title">Anonymous</span></span>
</label>
<label class="mv-choice">
<input type="checkbox" role="switch" class="mv-switch" data-size="sm" id="dl-timer">
<span class="mv-choice-text"><span class="mv-choice-title">60 s timer</span></span>
</label>
<button type="button" class="mv-button" data-variant="outline" data-size="sm" id="dl-restart">Restart</button>
</div>
</div>
<mv-deliberation id="dl-estimate" scale="fibonacci" unit="pts" rule="supermajority" me="priya" facilitator
question="Offline sync for the mobile app (MOB-412): how many points?"></mv-deliberation>
<p class="dl-hint">Pick your card, then Reveal votes: every card flips at once · Start the next round and watch the convergence chart narrow</p>
<div class="dl-sep"></div>
<div class="dl-bar">
<div>
<h3>Release go/no-go</h3>
<p>Read-only room display · anonymous votes · unanimity required</p>
</div>
</div>
<mv-deliberation id="dl-go" scale="go" rule="unanimous"
question="Roll Checkout v3 out to 100% of traffic on Thursday?"></mv-deliberation>
<script type="module">
const est = document.getElementById("dl-estimate");
const go = document.getElementById("dl-go");
const $ = (id) => document.getElementById(id);
const team = [
{ id: "priya", name: "Priya Nair" },
{ id: "aiko", name: "Aiko Tanaka" },
{ id: "mateo", name: "Mateo Rossi" },
{ id: "lars", name: "Lars Eriksen" },
{ id: "amara", name: "Amara Okafor" },
{ id: "diego", name: "Diego Fernández" },
];
const hour = 36e5;
const start = Date.now() - hour / 2;
const seed = () => [
{ votes: { priya: 5, aiko: 13, mateo: 1, lars: 5, amara: 8, diego: 3 }, revealed: true, startedAt: start, revealedAt: start + 4 * 6e4 },
{ votes: { aiko: 8, lars: 5, amara: 8, diego: 5 }, startedAt: Date.now() - 45e3 },
];
// Teammates' next votes, round by round (the viewer votes for themself).
const plans = {
2: { mateo: 5 },
3: { aiko: 8, mateo: 5, lars: 5, amara: 5, diego: 5 },
};
const later = { aiko: 5, mateo: 5, lars: 5, amara: 5, diego: 8 };
let timers = [];
const schedule = (round) => {
timers.forEach(clearTimeout);
timers = [];
const plan = plans[round] ?? later;
Object.entries(plan).forEach(([id, value], i) => {
timers.push(setTimeout(() => { if (est.isConnected && est.round === round) est.vote(id, value); }, 900 + i * 650 + Math.random() * 400));
});
};
est.people = team;
est.rounds = seed();
await customElements.whenDefined("mv-deliberation");
schedule(2);
est.addEventListener("mv-round", (e) => schedule(e.detail.round));
$("dl-anon").addEventListener("change", (e) => { est.anonymous = e.target.checked; });
$("dl-timer").addEventListener("change", (e) => { est.timer = e.target.checked ? "60s" : null; });
$("dl-restart").addEventListener("click", () => {
est.decision = null;
est.rounds = seed();
schedule(2);
});
go.people = [
{ id: "noor", name: "Noor Haddad" },
{ id: "felix", name: "Felix Wagner" },
{ id: "sade", name: "Sade Adeyemi" },
{ id: "kenji", name: "Kenji Watanabe" },
{ id: "lucia", name: "Lucía Morales" },
{ id: "oren", name: "Oren Levi" },
];
const day = Date.now() - 2 * hour;
go.anonymous = true;
go.rounds = [
{ votes: { noor: "go", felix: "no-go", sade: "conditional", kenji: "go", lucia: "go", oren: "conditional" }, revealed: true, anonymous: true, startedAt: day },
{ votes: { noor: "go", felix: "conditional", sade: "conditional", kenji: "go", lucia: "conditional", oren: "conditional" }, revealed: true, anonymous: true, startedAt: day + 9e5 },
{ votes: { noor: "conditional", felix: "conditional", sade: "conditional", kenji: "conditional", lucia: "conditional", oren: "conditional" }, revealed: true, anonymous: true, startedAt: day + 18e5 },
];
go.decision = {
value: "conditional",
note: "Ship behind the checkout-v3 kill switch. Payments on-call confirms a rollback in under 5 minutes before the ramp goes past 25%.",
rule: "unanimous",
round: 3,
agree: 6,
counted: 6,
at: day + 21e5,
};
</script>
</div>Kulturelle Referenz
Die zwölf Geschworenen, Sidney Lumet (nach dem Fernsehspiel von Reginald Rose aus dem Jahr 1954) (1957, Film). Eine Jury stimmt hinter verschlossenen Türen immer wieder ab, und Runde um Runde bringen die Zweifel eines Geschworenen, die angehört statt übergangen werden, das Ergebnis von 11 zu 1 bis zu einem einstimmigen Urteil. Übertragen auf die UI: Ein Team stimmt geheim ab, deckt gemeinsam auf, hört zuerst die Abstimmenden, die weit von der Gruppe entfernt liegen, stimmt erneut ab und sieht, wie sich die Streuung Runde um Runde verengt, bis seine Konsensregel erfüllt ist und das Urteil mit seinen Gründen festgehalten wird.
API
Attributes
| Name | Typ | Default | Description |
|---|---|---|---|
people | string | Participants as comma-separated names ("Priya Nair, Aiko Tanaka"); ids are slugs of the names. Use the people property for ids, short names and avatars. | |
scale | fibonacci | tshirt | go | hire | confidence | "1-10" | list | fibonacci | Voting scale. Presets: fibonacci (0 1 2 3 5 8 13 21 ?), tshirt (XS…XXL), go (No-go, Go with conditions, Go), hire (Strong no hire … Strong hire), confidence (1-5 with hints). A range "1-10", or a list "1, 2, 3, 5, 8, ?" / "a:Option A, b:Option B" (value:label). "?" is neutral: shown, never counted. |
measure | numeric | ordinal | nominal | How distances are read. Auto: numeric when every value is a number, otherwise ordinal (steps along the scale). nominal (unordered choices, e.g. which feature first) drops median, range and splits; outliers become lone votes against a majority. | |
me | person id | The viewer. Shows the private ballot (a radio group) for this person, their own card face up with a “You” tag. Without it the component is a read-only display. | |
facilitator | boolean | Shows the facilitator controls: Reveal votes, Start round N, and the decision form (value + rationale) once the rule allows it. | |
anonymous | boolean | Rounds revealed while it is on are anonymous: cards are laid out by value with no names, chips carry no initials, sentences give counts instead of names. The flag is stored per round, so turning it off later never unmasks a past round. | |
rule | unanimous | supermajority | majority | facilitator | supermajority | Consensus rule checked at each reveal. facilitator: never met automatically, the facilitator makes the call after any reveal. |
threshold | ratio ("2/3", "0.75", "80%") | 2/3 | Share of counted votes a supermajority needs (abstentions, “?” and missing votes are not counted). |
tolerance | number (steps) | 0 | Votes within this many steps of the candidate agree with it (1 = neighbors on the scale, e.g. 5 and 8). Ignored for nominal. |
outlier | number (steps) | 2 | Distance from the median, in scale steps, from which a minority vote is named as far from the group. Also the gap that makes two camps a split. |
timer | time ("60s", "2m", ms) | Per-round countdown shown in the header (role="timer"). At zero, mv-timeout fires (cancelable) and the round is revealed. | |
auto-reveal | boolean | Reveals as soon as every participant has voted (after a short beat). | |
abstain | "true" | "false" | true | "false" removes the Abstain option from the ballot. |
max-rounds | number | After this many rounds without consensus the facilitator may decide anyway (“Round limit reached”). | |
question | string | The question being decided, shown as the title and used as the group's accessible name. | |
unit | string | Unit appended to numeric values in sentences, stats and the decision ("pts", "days"). | |
locale | BCP 47 tag | en-US | Locale for lists (“Aiko and Mateo”), numbers and the decision date. |
label | string | Accessible name of the group when question is not set (default “Group decision”). | |
data-phase / data-revealing | set by the component | data-phase="voting | revealed | decided"; data-revealing while mv-reveal waitUntil promises are pending. |
Properties
| Name | Typ | Description |
|---|---|---|
people | Array<{ id?, name, short?, avatar?, role? }> | Participants. short is used in sentences (default: first word of name); avatar is an image URL (initials otherwise). |
options | Array<{ value, label?, short?, hint?, neutral? }> | Custom scale; overrides the scale attribute. short is used on cards and axes, hint under the ballot card, neutral options are shown but not counted. |
rounds | Array<{ votes: { [id]: value }, voted?: id[], revealed?, anonymous?, startedAt?, revealedAt? }> | Hydrate or read the whole history (silent: no events). The last round is the current one; voted lists people whose value is still secret. Reading returns copies with a round number. |
decision | { value, label, note, rule, met, round, agree, counted, at } | null | The recorded decision. Set it to hydrate a past decision; set null to reopen the discussion. |
round / phase / votes / stats | read-only | Current round number; "voting" | "revealed" | "decided"; current votes { id: value } (null for secret ones); analysis of the latest revealed round: { total, voted, counted, abstained, unsure, missing, distribution, median, mean, min, max, spread, candidate, agreement, outliers: [{ personId, value, distance }], split, consensus: { rule, met, value, agree, counted, needed } }. |
strings | Partial<Record<string, string>> | Overrides for every visible text, sentence and announcement ({placeholders}); English defaults. |
Methods
| Name | Description |
|---|---|
vote(personId, value) | Records a vote from your realtime feed: a scale value, "abstain", or null to withdraw. Emits the cancelable mv-vote (source "api"). Returns false outside the voting phase, for unknown people or values. |
markVoted(personId) | The person voted but the server keeps the value secret: the card turns face down with no value in the DOM. Supply values at reveal time. |
reveal(votes?) | Reveals the round (any number of votes). votes { id: value } fills secret values. Emits mv-reveal first, flips every card, then mv-revealed. Returns Promise<boolean>. |
nextRound() | Starts the next round with hidden votes; emits the cancelable mv-round. |
decide(value, note) | Records the decision for the current revealed round (any non-neutral scale value, whatever the rule: detail.met says whether it was met). Emits the cancelable mv-decision. |
reset() | Clears the history and the decision: one empty round. |
analyzeRound(votes, config) (module export) | The pure analysis used internally (Map of id → value, options, people, measure, rule, threshold, tolerance, outlierSteps), for server-side checks or tests. ABSTAIN is exported too. |
Events
| Name | Description |
|---|---|
mv-vote | Cancelable, before a vote is stored. detail: { personId, value (null = withdrawn), previous, round, source: "ui" | "api" }. Send ui votes to your backend here; preventDefault() rejects it (the ballot reverts). |
mv-reveal | Cancelable, before the reveal. detail: { round, source: "ui" | "api" | "timer" | "auto", voted, total, missing, waitUntil(promise) }. A waitUntil promise may resolve to { id: value } to disclose secret votes; a rejection cancels the reveal (announced, mv-reveal-error). |
mv-revealed | The cards have flipped. detail: { round, votes, stats, summary (the announced sentences), source }. |
mv-reveal-error | A waitUntil promise rejected. detail: { round, error }. |
mv-round | Cancelable, before a new round starts. detail: { round (the new number), previous (stats of the round just discussed), source }. |
mv-decision | Cancelable, before the decision is recorded. detail: { value, label, note, rule, met, round, agree, counted, stats, source }. |
mv-timeout | Cancelable: the round timer reached zero. preventDefault() keeps the round open (the timer is hidden). |
CSS classes
| Name | Description |
|---|---|
mv-deliberation-head / -kicker / -question / -rule / -timer | Header: round and phase, question, rule and anonymity chips, countdown with its ring (data-urgent in the last 10 s). |
mv-deliberation-seats / -seat / -card | The table. Each seat has data-state="waiting | voted | revealed | abstained | missing", data-me, data-own (own value shown), data-outlier, data-agree (with the met consensus), data-anonymous. |
mv-deliberation-toolbar / -progress / -reveal / -next | Vote count, progress bar and facilitator buttons (mv-button classes). |
mv-deliberation-ballot / -option / -option-face | The viewer's private ballot: a fieldset of native radios styled as cards; data-locked after the reveal. |
mv-deliberation-results / -stats / -dist / -col / -chip / -median / -insights | Revealed round: stats, distribution columns (data-candidate, data-window), voter chips (data-outlier), median marker and insight sentences (li[data-kind="count | consensus | pending | facilitator | outliers | split"]). |
mv-deliberation-history / -chart / -row / -track / -range / -dot / -med / -axis | Convergence chart: one row per round (data-current, data-pending for the round being voted), range bar, value dots sized by share, median marker, agreement share. |
mv-deliberation-decide / -record | Decision form (value chips, rationale textarea) and the recorded decision card. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-deliberation-accent | var(--mv-accent) | Ballot selection, consensus candidate, median marker. |
--mv-deliberation-outlier | var(--mv-warning) | Tint of the voters far from the group (always paired with a dashed outline, a flag and words). |
--mv-deliberation-agree | var(--mv-success) | Consensus reached: agreeing cards, chart dot, decision card. |
--mv-deliberation-card-back | var(--mv-accent) | Tint of the face-down card back pattern. |
Accessibility
The component is a labelled group (the question). Seats are a list: each item reads the name and a status (“Thinking…”, “Voted”, “Your vote: 5 pts”, then the revealed value, “far from group”, “Abstained” or “No vote”); the card graphics are aria-hidden, and hidden values never exist in the DOM before the reveal (except the viewer's own). The ballot is a native fieldset with a legend (“Your vote · Round 2”) and radios, so arrow keys, Tab and screen readers work as in any form; Abstain is a radio too and Withdraw is a real button. Reveal, Start round and Record decision are native buttons (aria-busy while revealing); after a reveal from the button, focus moves to Start round, after a new round to the ballot, after a decision to the decision card. Results are given three ways: visible sentences (counts, median and range, consensus, outliers named with their values, or the two camps of a split), a visually hidden table per round (voter, vote, note; value and count in anonymous rounds) and a visually hidden table of all rounds for the convergence chart, whose drawing is aria-hidden. A polite live region announces “Everyone has voted.”, the full summary at each reveal, each new round, the 10-second warning and the decision; a failed reveal is announced assertively. The timer is role="timer" with a spoken aria-label and is not live. Nothing relies on color alone: outliers have a dashed outline, a flag and the words “Far from group”, consensus has a check and a sentence, the median has a marker, anonymous rounds say so. Reduced motion (prefers-reduced-motion or data-motion="reduce"): cards swap faces instantly, no lift and no breathing on waiting seats. Forced colors: cards, tracks and chips use system colors, the selected option and the consensus use Highlight.