Welcome to Oshon · v1.0  ·  Now in public beta for enterprise teams Read the launch notes
Data VisualizationUpdatedPro· Data Visualization packWCAG 2.2 AA

Widget · Metric

Headline KPI tile — fixed 276×144 · big metric · picker · directional trend pill · popover kebab menu · drill-down title + value.

FigmaStorybookSource · PronpmPro

Unlock Widget · Metric

Pixel-aligned KPI tile authored for the dashboard half-quarter slot. Part of the Data Visualization pack.

Data Visualization pack starts at $249/yr Y1 (renews at $174/yr). Or get every Pro pack with Solo for $999/yr.

Preview

Live preview
@oshon-ai/components
Figma-parity authoring sample
Clickable Title NameSubheadername
60MMulti Picker
PY 18%
Drill-down — clickable title + clickable value
last 24h
+18%
Drill-down trace
Click the title (link styling) or the big metric to fire the drill-down handler.
Controlled picker — selection drives the value + trend
Active operatorslast 24h
60MRegion
PY 18%
Live state
  • region = all
  • value = 60M
  • trend = up · PY 18%
Trend tones — up · down · neutral
Sign-upslast 24h
12,840
PY 32%
Refund ratelast 24h
2.1%
PY 0.4%
API latency p99edge to commit
142ms
PY 0%
Kebab menu — popover-anchored, three actions
Pipeline valueopen opportunities
$4.2M
+12%
Menu trace
Click the ⋮ kebab to open the popover-anchored menu.
Dashboard row — three Metrics side-by-side
MRRnet of refunds
$680K
+8.4%
Churnrolling 30d
1.9%
−0.3pp
NPSlast quarter
62
PY 0

Installation

Install the runtime packages:

pnpm
pnpm add @oshon-ai/components @oshon-ai/tokens @oshon-ai/primitives

Or scaffold the component source directly into your codebase (shadcn-style):

pnpm
pnpm dlx @oshon-ai/cli add widgetmetric

Wire the tokens into your Tailwind v4 stylesheet:

css
/* app/globals.css */
@import 'tailwindcss';
@import '@oshon-ai/tokens/css';
@import '@oshon-ai/tokens/tailwind';

New here? Walk through the full setup — prereqs, theming, your first render.

Usage

Import the component and render it. Every component supports the standard tier, size, and disabled props where applicable.

tsx
'use client';
import { Widget·Metric } from '@oshon-ai/components';

export default function Example() {
  return <Widget·Metric />;
}
Pro · Data Visualization

Pixel-aligned KPI tile authored for the dashboard half-quarter slot. Part of the Data Visualization pack.

Unlock Widget · Metric

Pixel-aligned KPI tile authored for the dashboard half-quarter slot. Part of the Data Visualization pack.

Data Visualization pack starts at $249/yr Y1 (renews at $174/yr). Or get every Pro pack with Solo for $999/yr.

Styling

Three layers of customization, in order of escape-hatch strength: className overrides → data-attribute targeting → CSS custom properties.

Passing Tailwind classes

Every Oshon component accepts a className prop merged AFTER the component's default classes. Use it to override spacing, color, or size without forking the component.

tsx
<Widget·Metric
  className="ring-2 ring-offset-2 ring-blue-500"
/>

Data attributes

Oshon components expose their internal state as data-oshon-* attributes so you can target them from CSS without coupling to internal class names. The most common attributes are listed below — see the component's source for the full set.

AttributeValuesDescription
data-oshon-sizexs · s · m · l · mobileVisual size axis. Mirrors the `size` prop.
data-oshon-tierprimary · secondary · tertiaryVisual emphasis tier (Button family). Mirrors the `tier` prop.
data-oshon-stateenabled · active · error · disabledComponent surface state. Set automatically based on props.
data-disabledtrue · (omitted)Set when `disabled` is true. Pair with `:disabled` CSS for native input components.
data-stateopen · closed · checked · unchecked · …Radix-derived state for overlay components (Dialog, Tabs, Toggle, etc.).
css
/* Target the secondary tier specifically */
[data-oshon-tier="secondary"] {
  --oshon-color-primary-700: var(--my-brand-color);
}

Interactive states

Every interactive component supports the standard CSS pseudo- classes plus Tailwind's state variants. Focus rings always use :focus-visible so keyboard users see them but mouse users don't.

  • :hover / hover:* — pointer hover
  • :focus-visible / focus-visible:* — keyboard focus
  • :active / active:* — pressed
  • :disabled / disabled:* — set via the disabled prop

Anatomy

The named regions a consumer composes when rendering this component. Each is documented separately so you can target keyboard nav, ARIA labels, and slot props with precision.

header

Top 48 px strip — title (Lato Bold 14, black, -0.28px tracking), optional subtitle (Lato Regular 12, neutral-500), trailing 24×24 kebab. Top corners radius 8 px. Title becomes a `<button>` with link styling when `onTitleClick` is supplied.

value

Big headline metric centered horizontally between the header and the picker label — Lato Bold 24/32, -0.96px tracking, black. Becomes a `<button>` with focus ring when `onValueClick` is supplied. Use `valueLabel` to override the SR-announced text when the visible string is shorthand (e.g. visible "60M", labeled "60 million users").

picker-label

Caption text rendered above the picker trigger at left:12, top:88 — Lato Regular 12/16, gray-800. Hidden when no `pickerLabel` is supplied or the picker is omitted.

picker-trigger

Filter picker trigger at left:12, top:108 — 114 × 24 px box with 1 px neutral-300 border, 4 px corner radius, white surface, Lato Regular 10 px value text, trailing 16×16 chevron-down icon. Wraps a `Select.Root` so keyboard nav, typeahead, and the popover-anchored item list come from the Radix primitive. Hidden when `pickerOptions` is empty / omitted.

trend-pill

Right-side trend pill at left:150, top:108 — 114 × 24 px tinted box. Tones flow through the semantic palette so `applyTheme()` retints them: `up` ⇒ `--oshon-color-success-700` (bg color-mixed at 16% + full-saturation fg + chevron-up); `down` ⇒ `--oshon-color-error-700` + chevron-down; `neutral` ⇒ `--oshon-color-on-surface-muted` + em-dash. Label text is Lato Bold 12 uppercase tracking +0.48px. Hidden when `trendLabel` is omitted.

kebab

Three-dot vertical menu button at right:12 in the header. Three modes: (1) `moreItems` supplied → renders as a Popover.Trigger and the component owns the popover-anchored `Menu` open state (picks fire `onMoreAction(itemId)` and dismiss). (2) `onMore` supplied → plain `<button>` that fires the callback (consumer wires their own menu). (3) Neither → presentational `<span>` so the surface stays RSC-safe along the read-only path.

Keyboard

Header kebab is a `<button>` when `onMore` or `moreItems` is supplied (Tab to focus, Enter / Space to activate, Oshon focus ring); otherwise renders as a presentational `<span>` with `aria-hidden="true"`. Title becomes a `<button>` with link styling when `onTitleClick` is supplied. Big metric becomes a `<button>` with focus ring when `onValueClick` is supplied. Picker uses Radix Select primitives — keyboard navigation (↑/↓/Home/End/Enter/Esc/typeahead) is provided by the underlying primitive.

Accessibility

Every Oshon component ships axe-clean. We test in CI on every PR and publish the audit log per component.

WCAG level
2.2 AA
Screen readers tested
VoiceOver (macOS), NVDA (Windows)
Last axe audit
2026-04-29

Do / Don't

✓ Do

Default — Figma authoring sample (60M / All (2) / PY 18%)
<WidgetMetric
  title="Clickable Title Name"
  subtitle="Subheadername"
  value="60M"
  pickerLabel="Multi Picker"
  pickerOptions={[
    { value: 'all', label: 'All (2)' },
    { value: 'us',  label: 'US' },
    { value: 'eu',  label: 'EU' },
  ]}
  defaultPickerValue="all"
  trend="up"
  trendLabel="PY 18%"
  onMore={() => openMenu()}
/>
Drill-down — clickable title + clickable value
<WidgetMetric
  title="Active Users"
  value="60M"
  valueLabel="60 million active users"
  trend="up"
  trendLabel="+18%"
  onTitleClick={() => router.push('/dashboards/users')}
  onValueClick={() => router.push('/dashboards/users/detail')}
/>
Popover-anchored kebab menu via `moreItems`
<WidgetMetric
  title="Active Users"
  value="60M"
  trend="up"
  trendLabel="+18%"
  moreItems={[
    { id: 'view', label: 'View details' },
    { id: 'export', label: 'Export as CSV' },
  ]}
  onMoreAction={(id) => handle(id)}
/>
Composing a dashboard row with sibling widgets
<div style={{ display: "flex", gap: 8, alignItems: "flex-start" }}>
  <WidgetMetric title="Users" value="60M" trend="up" trendLabel="+18%" />
  <WidgetMetric title="Sessions" value="240M" trend="down" trendLabel="-3%" />
</div>

✗ Don't

Hardcoded dimension override
<WidgetMetric title="x" value="60M" style={{ width: 400, height: 200 }} />

WidgetMetric is authored for the half-height quarter slot — 276 × 144 px. Other slot sizes are served by sibling Widget* components. Forcing different outer dims breaks the dashboard grid math and ships a visual misalignment.

Trend tone disagreeing with the trendLabel sign
<WidgetMetric title="x" value="60M" trend="up" trendLabel="-15%" />

`trend` is the visual signal of business-significance, but a `down` label paired with `up` tone produces a visually confusing pill. Painting `-15%` as up is supported (e.g. cost reduction), but the consumer is responsible for keeping the tone aligned with the dashboard's domain semantics. When in doubt, match the sign.

Ad-hoc Select beside the widget instead of the built-in picker
<><WidgetMetric title="x" value="60M" /><Select.Root /* … */ /></>

The widget owns the picker / metric / trend layout — placing a sibling Select breaks the 276 × 144 footprint and the dashboard grid math. Pass `pickerOptions`, `pickerValue`, and `onPickerChange` into the widget itself; the picker slot is wired to a Radix Select internally.

Design rationale

WidgetMetric is the fourth member of the dashboard-widget family — a fixed-dim 276×144 surface authored for the half-height quarter slot, complementing the full-quarter WidgetTrackingItem / WidgetLeaderboard / WidgetDonutChart. It is tuned for the canonical "headline number + filter + trend" pattern (e.g. "60M users · All regions · +18% YoY"). The big metric value (Lato Bold 24, -0.96 tracking, centered) is the visual focal point; the picker delegates to the package's own Select compound so keyboard nav / typeahead / portal positioning come from one tested primitive. The trend pill paints in three utility tones (green / red / neutral) and renders a directional chevron — tone is consumer-driven (business-significance is a domain decision). Outer dims do NOT scale with `size`; only typography density does, mirroring the rest of the widget family so dashboard rows stay grid-aligned regardless of which size token the host app applies. `onTitleClick`, `onValueClick`, and the popover-anchored kebab Menu mirror sibling widgets exactly so dashboard widgets feel like a coherent family. Figma file `MsyCsSxIRkgRjWd1bSJpq6` node `12002:5432`.