DecisionCard
A verdict, the figures behind it, and an action that exists only when the verdict allows one.
import { DecisionCard } from '@elirobinson/react/components/molecules/DecisionCard';Styles: @elirobinson/react/styles/molecules/DecisionCard.css — already included when you import @elirobinson/react/styles.css.
Annual maintenance plan
The plan costs less than the two repairs a year this unit averages.
- Plan
- $480
- Average repair
- $310
- Repairs covered
- Unlimited
Cost this year$480
Paying per repair$620
Priced for one unit. A second unit adds $180 and changes the maths.
Show code
import { Button } from '@elirobinson/react/components/atoms/Button';
import { DecisionCard } from '@elirobinson/react/components/molecules/DecisionCard';
export default function Basic() {
return (
<DecisionCard
verdict="go"
verdictLabel="Worth it"
subject="Annual maintenance plan"
headline="The plan costs less than the two repairs a year this unit averages."
figures={[
{ label: 'Plan', value: '$480', kind: 'cost' },
{ label: 'Average repair', value: '$310', kind: 'cost' },
{ label: 'Repairs covered', value: 'Unlimited', kind: 'coverage' },
]}
total={{ label: 'Cost this year', value: '$480' }}
contrast={{ label: 'Paying per repair', value: '$620' }}
caveat="Priced for one unit. A second unit adds $180 and changes the maths."
action={<Button variant="accent">Renew the plan</Button>}
/>
);
}When to use it
DecisionCard is for a surface that has reached a conclusion and has to show its work: a
verdict at the head, the figures behind it, a contrasting figure to measure against, the
caveat that qualifies it, and — only sometimes — an action. It composes VerdictBadge for
the marker, so the verdict is a glyph and a word rather than a colour.
Use it when the reader's next question is "why?" A surface that only announces an outcome doesn't need this; a surface that has to justify one does.
Named for the shape, not the subject
DecisionCard is named for its shape — verdict, figures by kind, contrast figure, caveat,
conditional action — and not for any particular thing a product puts in it. That shape is
a quote, an eligibility result, or a risk assessment just as readily as it is a
recommendation, which is why the name says "decision" and not something narrower.
The same reasoning drives kind on a figure. It renders as data-kind rather than
resolving to a class from a fixed enum, so a product can group its figures — costs,
coverage, timing, whatever it actually tracks — and style those groups from its own
stylesheet, without the system having to learn the product's vocabulary or grow a union
type every time a product invents a category.
The footer guarantee
Extended device coverage
The coverage costs more than the repair it protects you from.
- Coverage
- $240
- Screen repair
- $150
- Deductible
- $79
Cost over two years$240
One out-of-pocket repair$150
A second repair inside the term would change this.
I'd skip it and put the $240 toward the replacement instead.
Show code
import { DecisionCard } from '@elirobinson/react/components/molecules/DecisionCard';
export default function NoAction() {
return (
<DecisionCard
verdict="no"
verdictLabel="Skip it"
subject="Extended device coverage"
headline="The coverage costs more than the repair it protects you from."
figures={[
{ label: 'Coverage', value: '$240', kind: 'cost' },
{ label: 'Screen repair', value: '$150', kind: 'cost' },
{ label: 'Deductible', value: '$79', kind: 'cost' },
]}
total={{ label: 'Cost over two years', value: '$240' }}
contrast={{ label: 'One out-of-pocket repair', value: '$150' }}
caveat="A second repair inside the term would change this."
closing="I'd skip it and put the $240 toward the replacement instead."
/>
);
}When action is absent, DecisionCard renders no .ds-decision__foot element at all
— not a disabled button, not a hidden one, nothing. This is a product guarantee, not a
style choice, and it has its own test in the package.
The reason is blunt. A decision surface that can render a "pay now" control under a "do
not buy" verdict is one CSS bug away from taking a user's money against its own advice,
and a disabled button is exactly that bug waiting for a stylesheet to lose. One override,
one stray disabled={false}, one screen reader that ignores hidden, and the control is
live again. The only way to be certain that can't happen is for the element not to exist —
so a negative verdict is structurally incapable of offering a payment control.
closing is what carries the last word instead. It renders in the body, above where a
footer would have been, so a card that recommends against something still ends on a
sentence rather than trailing off. Pass closing on every negative verdict; it's the
replacement for the button you're not shipping.
If you need a control near a "no" — "remind me later," "see other options" — put it outside the card, where it can be labelled for what it actually does and can't inherit the card's authority.
Heading level
headline renders as a real heading element, not a styled paragraph — it is the card's
title in the document outline, so a screen reader's heading navigation has to be able to
find it. There is no single correct level for an arbitrary outline, so headingLevel is
exposed rather than hardcoded: leave the default (headingLevel={2}, an <h2>) for a card
sitting under the page's own <h1>, and pass headingLevel={3} for one inside an <h2>
section. An out-of-range value falls back to 2 rather than crashing the tree.
The range starts at 2 where Accordion's starts at 1. An accordion can legitimately be the
only thing on a page; a card that claimed the document's <h1> would be claiming to be the
page. The type ramp is carried by the class, so every level looks identical and only the
outline changes.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
headlinerequired | string | — | — |
verdictrequired | "go" | "no" | "hold" | — | — |
verdictLabelrequired | string | — | — |
action | ReactNode | — | — |
caveat | string | — | — |
closing | string | — | — |
contrast | { label: string; value: string; } | — | — |
figures | DecisionFigure[] | — | — |
headingLevel | 2 | 3 | 4 | 5 | 6 | 2 | Heading level (2-6) for `headline`, which renders as the real heading element rather than a styled paragraph. There is no single correct level for an arbitrary document outline, so this is exposed rather than hardcoded — a card sitting under a page `<h1>` leaves the default (`headingLevel={2}`), one inside an `<h2>` section passes `headingLevel={3}`. Defaults to 2. |
subject | string | — | — |
total | { label: string; value: string; } | — | — |
Also accepts all HTMLAttributes<HTMLDivElement> props.
Accessibility
headlineis a real<h2>–<h6>(see "Heading level" above), so the card has a title in the document outline and heading navigation lands on it.- The figures render as a real
<dl>: each figure'slabelis a<dt>and itsvaluea<dd>, so the pairing holds without the grid. - Figures are keyed by
kindandlabeltogether, so the same label can appear under two different kinds without a key collision. totalandcontrastrender as paragraphs rather than more<dl>entries — they're a summation and a comparison, not further items of the same list.- The verdict marker comes from
VerdictBadge: its glyph isaria-hiddenandverdictLabelis the accessible text. Write it as the verdict in words. - The card itself has no role and no live region. If a verdict updates in place, announce that from wherever your product owns status messages.
- Keyboard: only the node you pass as
actionis focusable. When there's no action there's nothing to tab to, which is the intended outcome, not an oversight. - The ref forwards to the outer
<div>.
Do
- Pass closing on every negative verdict — it is what replaces the missing footer.
- Give the reader the contrast figure; a number with nothing to measure against persuades nobody.
- State the caveat that would change the verdict, in the caveat slot.
- Use kind to group figures your product already groups, and style them from your own stylesheet.
Don't
- Pass a disabled button as action to "grey out" an offer — omit action instead.
- Reintroduce a footer with a wrapper element that renders whether or not there is an action.
- Put a payment control outside the card and style it to look like it belongs to a "no" verdict.
- Use it for a plain summary with no judgement in it; that is Card or StubCard.
Product theming
DecisionCard inherits the product token layer through the VerdictBadge it composes:
the verdict fills and foregrounds read --product-verdict-* with a fallback to the
matching system status token. The convention is documented in
docs/agents/product-token-layer.md, and the fallback keeps it optional — a product that
declares nothing gets the system palette.