DecisionCard

A verdict, the figures behind it, and an action that exists only when the verdict allows one.

moleculesSource
import { DecisionCard } from '@elirobinson/react/components/molecules/DecisionCard';

Styles: @elirobinson/react/styles/molecules/DecisionCard.css — already included when you import @elirobinson/react/styles.css.

Worth it

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.

Skip it

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

PropTypeDefaultDescription
headlinerequiredstring
verdictrequired"go" | "no" | "hold"
verdictLabelrequiredstring
actionReactNode
caveatstring
closingstring
contrast{ label: string; value: string; }
figuresDecisionFigure[]
headingLevel2 | 3 | 4 | 5 | 62Heading 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.
subjectstring
total{ label: string; value: string; }

Also accepts all HTMLAttributes<HTMLDivElement> props.

Accessibility

  • headline is 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's label is a <dt> and its value a <dd>, so the pairing holds without the grid.
  • Figures are keyed by kind and label together, so the same label can appear under two different kinds without a key collision.
  • total and contrast render 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 is aria-hidden and verdictLabel is 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 action is 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.