Patterns

Ranked bars

A ranking as horizontal bars, biggest first: top decline reasons, volume by provider. The value is written at each bar's tip, the bars share one tone, and past the tenth the rest fold into Other. The most common chart merchants ask for. Counts or amounts only; a rate per group is a table.

Updated Oct 12, 2026 by Juan Pablo Turina

Anatomy

One row per entity: the label, the bar, and the value at its tip. With drilldown, a 'View payments' column on the right. The bars are sized against the biggest one, so every bar starts at zero and there is no axis to read.

  • Insufficient funds
    3,170
  • Do not honor
    1,779
  • Issuer unavailable
    928
  • 3DS failed
    696
  • Expired card
    402
  • Invalid CVV
    351
  • Suspected fraud
    288
  • Limit exceeded
    204
  • Restricted card
    167
  • Invalid account
    122
  • Other
    138
  1. 1
    Label

    The entity, up to 10rem, truncated. Other is written in muted text.

  2. 2
    Bar

    h-5 (20px), square ends, var(--series-1). Its length is the value against the biggest one, from zero.

  3. 3
    Value

    At the tip, in tabular figures, formatted by formatValue. It is the reading.

  4. 4
    Highlight

    With highlight, the named entity keeps the tone and the rest turn grey.

  5. 5
    Other

    Past the tenth, one Other row, always last and in var(--series-other). It never drills.

  6. 6
    Drilldown

    With onViewPaymentsRow, rows that carry a filter reveal 'View payments' on hover.

Rules

What the bars enforce, and what the caller must not ask of them.

Do
  • Pass counts or amounts: declines, payments, volume.
  • Let the bars sort and fold. Top ten and Other is the rule.
  • Name the entity the question asked about with highlight.
  • Put it in a ChartCard body, with the total as the card's value (7,733 declines) and the window in its subtitle.
Don't
  • Don't pass rates. Other would add up rates, and a rate per group is a table.
  • Don't give each bar its own colour: one measure, one tone. The length already says which is bigger.
  • Don't draw it with a visible axis or gridlines: the value at the tip is the reading.
  • Don't give Other a filter or put it anywhere but last.

Recipes

In a ChartCard

The common shape: top decline reasons, with the total as the card's value and the window in its subtitle.

Decline reasons

7,733declines
Last 7 days
  • Insufficient funds
    3,170
  • Do not honor
    1,779
  • Issuer unavailable
    928
  • 3DS failed
    696
  • Expired card
    402
  • Other
    758
<ChartCard title="Decline reasons" value="7,733" suffix="declines" subtitle="Last 7 days">
  <RankedBars
    rows={[
      { key: "insufficient_funds", label: "Insufficient funds", value: 3170 },
      { key: "do_not_honor",       label: "Do not honor",       value: 1779 },
      { key: "issuer_unavailable", label: "Issuer unavailable", value:  928 },
      // …
      { key: "others", label: "Others", value: 758 }, // already folded upstream: joins Other
    ]}
    formatValue={(n) => n.toLocaleString("en-US")}
  />
</ChartCard>
One entity named

The question named Stripe: it keeps the tone and the other providers step back to grey.

  • dLocal
    $6.3M
  • Stripe
    $3.1M
  • Adyen
    $2.2M
  • Braintree
    $1.4M
  • PayU
    $891K
// "How much went through Stripe?" names one provider: it keeps the tone.
<RankedBars rows={providers} formatValue={formatMoney} highlight="stripe" />
More than ten entities

Thirteen reasons in: the ten biggest, then Other, last and in grey, with the sum of the rest.

  • Insufficient funds
    3,170
  • Do not honor
    1,779
  • Issuer unavailable
    928
  • 3DS failed
    696
  • Expired card
    402
  • Invalid CVV
    351
  • Suspected fraud
    288
  • Limit exceeded
    204
  • Restricted card
    167
  • Invalid account
    122
  • Other
    138
// Thirteen reasons in: the ten biggest, then Other, last and in grey.
<RankedBars rows={allThirteenReasons} formatValue={formatCount} otherLabel="Other" />
With drilldown

Wire onViewPaymentsRow. Rows with a filter reveal 'View payments' on hover; the column is kept on every row so the bars keep one track.

  • Insufficient funds
    3,170
  • Do not honor
    1,779
  • Issuer unavailable
    928
  • 3DS failed
    696
  • Expired card
    402
  • Invalid CVV
    351
  • Suspected fraud
    288
  • Limit exceeded
    204
  • Restricted card
    167
  • Invalid account
    122
  • Other
    138
<RankedBars
  rows={rows}                 // each row carries filter: { reason: "insufficient_funds" }
  formatValue={formatCount}
  onViewPaymentsRow={(row) => navigate({ dimensions: row.filter })}
/>

Import

Copy this import line at the top of the file where you compose this molecule.

RankedBars is display only: no state. The tone is fixed, and highlight and drilldown come from the caller.
import { RankedBars, type RankedBarsRow } from "@/components/patterns/ranked-bars";

Props

Everything else is forwarded to the underlying elements via ...props.

RankedBars

PropTypeDefaultDescription
rowsRankedBarsRow[] (required)—Counts or amounts, in any order: the bars sort them biggest first and fold past the tenth into Other.
formatValue(n: number) => string (required)—Formats the value written at each bar's tip, e.g. a count or a currency formatter.
highlightstring—The key of the entity the question named. It keeps var(--series-1); the other bars turn grey.
onViewPaymentsRow(row: RankedBarsRow) => void—Rows with a filter reveal a 'View payments' CTA on hover. Other never drills: it is several entities.
otherLabelstring"Other"Label of the folded tail.
classNamestring—Extra classes on the list.

RankedBarsRow

PropTypeDefaultDescription
keystring (required)—Stable id of the entity; what highlight matches.
labelstring (required)—Row label. A row labelled Other, Others or Otros joins the folded tail.
valuenumber (required)—A count or an amount. Never a rate: Other is a sum. Negative and missing values are left out.
filterRecord<string, string | number>—Per-row drilldown filter, used with onViewPaymentsRow.

Related

Cross-links to the atoms this molecule composes and sibling patterns.

  • Chart cardThe usual host: RankedBars goes in its body.
  • Share barWhen the parts add up to a whole, the share is one bar instead.
  • TableA rate per group (approval by provider) is a table, not a ranking.
  • ChartThe chart Rules this piece follows.