Components

Provider legend row

Horizontally scrollable pill row used above provider-heavy charts (Approval rate by provider, Error rate, Latency p90/p95/p99, Timeout rate). Each pill stacks [dot + name muted] then [value 2xl semibold tabular]. When onSelectKey is wired, pills become buttons — clicking one toggles it selected; other pills dim to opacity-40 so the chart consumer can filter to a single provider. Click the same pill again to clear.

Updated Aug 6, 2026 by Leonardo Posada

Anatomy

A horizontally scrollable row of pills. The outer container bleeds -mx-6 px-6 so the first/last pill isn't clipped by the ChartCard's p-6. Each pill has two rows: first = dot + name (text-sm muted); second = value (text-2xl semibold tabular-nums foreground). min-w-28 (112px) per pill so widths stay consistent.

Stripe
89.13%
Adyen
84.51%
Braintree
79.20%
dLocal
75.02%
Cybersource
71.44%
PayU
68.31%
Worldpay
64.90%
Checkout.com
62.11%
  1. 1
    Scroll container

    -mx-6 overflow-x-auto px-6 pb-2. Lets 6+ providers scroll horizontally inside a ChartCard without clipping.

  2. 2
    Pill (static)

    min-w-28 flex-col gap-1 px-2 py-1. Used when onSelectKey is not defined. Pure <div>.

  3. 3
    Pill (interactive)

    Same layout but rendered as a <button> with aria-pressed. Hover = bg-accent. Selected = bg-muted. When any pill is selected, unselected pills get opacity-40.

  4. 4
    Dot

    size-2 rounded-full. Color from provider.color — prefer var(--chart-N).

  5. 5
    Name

    text-sm text-muted-foreground. Truncates with min-w-0 sibling.

  6. 6
    Value

    text-2xl font-semibold tabular-nums text-foreground. Current KPI (%, ms).

Recipes

Static (display-only)

Omit onSelectKey. Pills render as <div>s with no hover / click behavior. Use when the chart doesn't support filter-to-one-provider or when the row is purely informational.

Stripe
89.13%
Adyen
84.51%
Braintree
79.20%
dLocal
75.02%
Cybersource
71.44%
PayU
68.31%
Worldpay
64.90%
Checkout.com
62.11%
<ProviderLegendRow providers={providers} />
// providers: ProviderLegendItem[] = [
//   { key, name, color, value },
// ]
Interactive (click to filter)

Wire selectedKey + onSelectKey to enable click-to-filter. The chart consumer reads selectedKey and filters its series to the matching provider (or renders all when null). Selection dims non-selected pills to opacity-40.

const [selected, setSelected] = React.useState<string | null>(null);

<ProviderLegendRow
  providers={providers}
  selectedKey={selected}
  onSelectKey={setSelected}
/>
// Read `selected` in the sibling chart to filter its series.

Import

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

ProviderLegendRow is controlled. State (selectedKey) belongs to the parent so both the row AND the sibling chart can react to it. Toggling a pill twice clears the selection back to null.
import { ProviderLegendRow, type ProviderLegendItem } from "@/components/ui/provider-legend-row";

Props

Everything else from the underlying HTML or Radix primitive is forwarded via ...props.

ProviderLegendRow

PropTypeDefaultDescription
providersProviderLegendItem[] (required)The pills to render. See item shape below.
selectedKeystring | nullSelected provider key. Pass null for no selection. When defined together with `onSelectKey`, pills become interactive buttons.
onSelectKey(key: string | null) => voidFires when the user toggles a pill. Receives the new key, or null to clear. Absent = pills stay static.
classNamestringExtra classes on the outer overflow-x-auto wrapper.

ProviderLegendItem

PropTypeDefaultDescription
keystring (required)Stable key. Used for React reconciliation AND for `selectedKey` matching.
namestring (required)Provider name (dot + name row, muted).
colorstring (required)Dot color. Prefer var(--chart-N).
valuestring (required)Current KPI ("89.13%", "1,287 ms"). Rendered text-2xl semibold tabular-nums.

Related

Cross-links to atoms and patterns you may reach for next.

  • Status legendStatic wrap-friendly sibling. Use when the legend should stack instead of scroll and interactivity isn't needed.
  • Chart cardCanonical host — sits at the top of the ChartCard body above the chart.
  • ChartThe chart consumer reads selectedKey and filters its series accordingly.