Patterns

Share bar

Parts of a whole as one horizontal bar that adds up to the total, with the ranked list under it: dot, label, value and share. It replaces the donut in a new flow, because a share reads faster as lengths on one line than as angles around a ring. Counts or amounts only.

Updated Oct 14, 2026 by Juan Pablo Turina

Anatomy

Two parts. The bar: one segment per entity, sized by its value, with a 1px gap that shows the surface behind it. The list: a LegendList row per segment. Hovering a segment lights its row, and hovering a row dims the other segments.

  • Card
    $9.2M54.4%
  • Bank transfer
    $5.5M32.5%
  • Wallet
    $1.5M8.9%
  • Cash voucher
    $720K4.3%
  1. 1
    Bar

    h-4 (16px), rounded-full, full width. Segments grow in proportion to their value, biggest first, or in the caller's order with order="given".

  2. 2
    Segment

    The entity's colour, var(--series-N), given by the caller. Never picked by position.

  3. 3
    Gap

    1px between segments. It is the surface itself, so the bar reads on a card and on a panel alike.

  4. 4
    Other

    Past the sixth entity, the rest folds into one Other segment and row, always var(--series-other) and always last. A row the caller already named Other joins it. Other never drills: it is several entities.

  5. 5
    List row

    LegendList: dot, label, the value in muted tabular figures, and the share in a secondary Badge with one decimal.

  6. 6
    Drilldown

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

Rules

What the bar enforces, and what the caller must not ask of it.

Do
  • Pass counts or amounts: payments, volume, disputes.
  • Give every row its entity's colour, var(--series-N), the same one it wears on every other chart.
  • Let the bar sort and fold. Six segments and Other is the rule.
  • For parts that have their own order, like payment states, pass them in that order (byPaymentState) with order="given": Refunded never sits next to the red.
  • Put it in a ChartCard body, or straight into a narrow panel.
Don't
  • Don't pass rates. An approval rate per provider does not add up to anything: that is a ranked list or a table.
  • Don't colour by position in the list: a filter that removes a neighbour would repaint every survivor.
  • Don't use a donut for a new flow. The donut recipes copy Insights and are marked as production mirrors.
  • Don't give Other a filter or a colour of its own.

Recipes

In a ChartCard

The common shape: a share of approved volume by payment method, inside a ChartCard.

Approved volume by payment method

  • Card
    $9.2M54.4%
  • Bank transfer
    $5.5M32.5%
  • Wallet
    $1.5M8.9%
  • Cash voucher
    $720K4.3%
<ChartCard title="Approved volume by payment method">
  <ShareBar
    rows={[
      { key: "card",   label: "Card",          value: 9_200_000, color: "var(--series-1)" },
      { key: "wallet", label: "Wallet",        value: 1_500_000, color: "var(--series-2)" },
      { key: "bank",   label: "Bank transfer", value: 5_500_000, color: "var(--series-3)" },
      { key: "cash",   label: "Cash voucher",  value:   720_000, color: "var(--series-4)" },
    ]}
    formatValue={formatMoney}
  />
</ChartCard>
More than six entities

Eight providers in: the bar keeps the six biggest and folds the rest into Other, in grey. The shares still add up to 100%.

Approved volume by provider

  • dLocal
    $6.3M43.2%
  • Stripe
    $3.1M21.4%
  • Adyen
    $2.2M15.0%
  • Braintree
    $1.4M9.7%
  • PayU
    $891K6.1%
  • Worldpay
    $410K2.8%
  • Other
    $261K1.8%
// Eight providers in, six segments and Other out. Past the sixth, rows fold
// into Other, drawn in --series-other whatever colour they carried.
<ShareBar rows={providers} formatValue={formatMoney} otherLabel="Other" />
Payment states, in their order

Statuses sorted with byPaymentState and drawn with order="given": each state in its own colour (paymentStateColour), Declined and Error one red series, Refunded never next to the red, and the states the launch does not name folded into Other, last and grey.

Payment status distribution

  • Succeeded
    1,20469.2%
  • Refunded
    1589.1%
  • Pending
    543.1%
  • Declined / error
    22212.8%
  • Other
    1015.8%
import { ShareBar, type ShareBarRow } from "@/components/patterns/share-bar";
import { byPaymentState, paymentStateColour, paymentStateSeries } from "@/lib/payment-state";

// Counts by status, as the payments list reads them.
const counts: [string, number][] = [["Succeeded", 1204], ["Declined", 180], ["Refunded", 158], ["Error", 42] /* … */];

// One row per series: Declined and Error are one red series, unnamed states are Other.
const SERIES_LABEL: Partial<Record<string, string>> = { failed: "Declined / error", other: "Other" };
const rows: ShareBarRow[] = Object.values(
  counts.reduce<Record<string, ShareBarRow>>((acc, [status, value]) => {
    const series = paymentStateSeries(status);
    const label = SERIES_LABEL[series] ?? status;
    acc[series] = { key: series, label, value: (acc[series]?.value ?? 0) + value, color: paymentStateColour(status) };
    return acc;
  }, {}),
).sort((a, b) => byPaymentState(a.key, b.key));

// order="given": the states keep their order, so Refunded never sits next to the red.
<ShareBar rows={rows} order="given" formatValue={(n) => n.toLocaleString("en-US")} />
With drilldown

Wire onViewPaymentsRow. Rows with a filter reveal 'View payments' on hover; Other never does.

  • Card
    $9.2M54.4%
  • Bank transfer
    $5.5M32.5%
  • Wallet
    $1.5M8.9%
  • Cash voucher
    $720K4.3%
<ShareBar
  rows={rows}                 // each row carries filter: { method: "card" }
  formatValue={formatMoney}
  onViewPaymentsRow={(row) => navigate({ dimensions: row.filter })}
/>
In a narrow panel

At 420px the bar keeps every segment and the list keeps every row. Nothing is cut, because a bar whose list is missing rows no longer adds up visibly.

  • Card
    $9.2M54.4%
  • Bank transfer
    $5.5M32.5%
  • Wallet
    $1.5M8.9%
  • Cash voucher
    $720K4.3%
<div className="w-[420px]">
  <ShareBar rows={rows} formatValue={formatMoney} />
</div>

Import

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

ShareBar is display plus hover: it keeps which segment is hovered and nothing else. Colour and drilldown come from the caller.
import { ShareBar, type ShareBarRow } from "@/components/patterns/share-bar";

Props

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

ShareBar

PropTypeDefaultDescription
rowsShareBarRow[] (required)—Counts or amounts. The bar sorts them biggest first (or keeps their order with order="given") and folds past the sixth into Other.
formatValue(n: number) => string (required)—Formats each row's value in the list, e.g. a currency or a count formatter.
onViewPaymentsRow(row: ShareBarRow) => void—Rows with a filter reveal a 'View payments' CTA on hover. Other never drills: it is a sum of several entities.
otherLabelstring"Other"Label of the folded tail.
order"size" | "given""size""size": biggest first. "given": the rows' own order, for parts that have one, like payment states sorted with byPaymentState. Either way a row named Other joins the folded tail and is last.
classNamestring—Extra classes on the wrapper.

ShareBarRow

PropTypeDefaultDescription
keystring (required)—Stable id of the entity.
labelstring (required)—Segment and row label.
valuenumber (required)—A count or an amount. Never a rate: rates do not add up. Zero and negative values are left out.
colorstring (required)—The entity's colour, var(--series-1..6), from entityColours() (@/lib/entity-colour) asked by name. Required on purpose: ShareBar never assigns colour by position.
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: ShareBar goes in its body.
  • Legend listThe list under the bar. Use it alone for a ranked list of rates.
  • Donut cardThe Insights donuts, marked as production mirrors. ShareBar is what a new flow uses instead.
  • ColorsThe Series section: --series-1..6 and --series-other.