Preview
Installation
Install the runtime packages:
pnpm add @oshon-ai/components @oshon-ai/tokens @oshon-ai/primitives
Or scaffold the component source directly into your codebase (shadcn-style):
pnpm dlx @oshon-ai/cli add widgettrackingitem
Wire the tokens into your Tailwind v4 stylesheet:
/* 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.
'use client';
import { Widget·TrackingItem } from '@oshon-ai/components';
export default function Example() {
return <Widget·TrackingItem />;
}2×2 metric tracker with per-cell drill-down + semantic trend tones. Part of the Data Visualization pack.
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.
<Widget·TrackingItem 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.
| Attribute | Values | Description |
|---|---|---|
data-oshon-size | xs · s · m · l · mobile | Visual size axis. Mirrors the `size` prop. |
data-oshon-tier | primary · secondary · tertiary | Visual emphasis tier (Button family). Mirrors the `tier` prop. |
data-oshon-state | enabled · active · error · disabled | Component surface state. Set automatically based on props. |
data-disabled | true · (omitted) | Set when `disabled` is true. Pair with `:disabled` CSS for native input components. |
data-state | open · closed · checked · unchecked · … | Radix-derived state for overlay components (Dialog, Tabs, Toggle, etc.). |
/* 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 thedisabledprop
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.
headerTop 48 px strip — title (Lato Bold 14, black, -0.28px tracking), 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.
section-labelLato Bold 14 uppercase string at left:20, top:60 — e.g. "TRACKING ITEM". +0.56px tracking, gray-800. Identifies the widget category at a glance and reads as a visual anchor between the header and the metric grid.
cellOne of four metric cells in the 2×2 grid. Layout: header row at top:12 (label + optional `(suffix)` + optional info-icon Tooltip trigger), caption row at top:36 (Lato Regular 11, neutral-500), value row near bottom (Lato Bold 16/20 in the trend tone — neutral / `--oshon-color-success-700` / `--oshon-color-error-700` — followed by optional secondary value in Lato Regular 14/20 gray-600 and optional 14×14 trend chevron). `applyTheme()` retints the trend tones via the semantic palette. Cell becomes a `<button>` with hover background tint + focus ring when `onClick` is supplied.
kebabThree-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.
info-iconPer-cell 14×14 info-circle that opens a Tooltip on hover / focus. Rendered only when the metric carries a `tooltip` string. The icon is a `<button>` so the tooltip is reachable via keyboard focus, in line with the WAI-ARIA tooltip pattern.
Keyboard
Header kebab is a `<button>` when `onMore` or `moreItems` is supplied (Tab to focus, Enter / Space to activate, Oshon focus ring); otherwise it renders as a presentational `<span>` with `aria-hidden="true"`. Title becomes a `<button>` with link styling when `onTitleClick` is supplied. Each metric cell becomes a full-cell `<button>` when its `onClick` is supplied — keyboard-activatable with the same focus + hover treatment as the title. Header info-icons are themselves `<button>`s that wrap a `Tooltip` so the tooltip is reachable via keyboard focus.
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
<WidgetTrackingItem
title="Beef Price Tracker"
subtitle="Last 12 months"
sectionLabel="TRACKING ITEM"
primary={[
{ label: 'Avg. Price', labelSuffix: '(per lb)', caption: "JUL '21/'20",
value: '$2.48', secondaryValue: '/$2.55' },
{ label: '% Change', caption: "JUL '21/'20",
value: '-3.0%', secondaryValue: '/-$0.07' },
]}
comparison={[
{ label: 'Comparison', tooltip: 'Year-over-year delta',
caption: "JUL '21/'20", value: '+5.0%', trend: 'up' },
{ label: 'Avg. Price vs PPI', tooltip: 'Producer Price Index',
caption: '% Change', value: '-8.0%', trend: 'down' },
]}
onMore={() => openMenu()}
/><WidgetTrackingItem
title="Tracker"
sectionLabel="TRACKING ITEM"
primary={[
{ ...m1, onClick: () => router.push('/tracker/avg-price') },
{ ...m2, onClick: () => router.push('/tracker/change') },
]}
comparison={[m3, m4]}
onTitleClick={() => router.push('/dashboards/tracking')}
/><WidgetTrackingItem
title="Tracker"
sectionLabel="TRACKING ITEM"
primary={[m1, m2]}
comparison={[m3, m4]}
moreItems={[
{ id: 'view', label: 'View details' },
{ id: 'export', label: 'Export as CSV' },
]}
onMoreAction={(id) => handle(id)}
/><div style={{ display: "flex", gap: 8, alignItems: "flex-start" }}>
<WidgetLeaderboard title="Top Vendors" items={items} />
<WidgetTrackingItem title="Tracker" sectionLabel="TRACKING ITEM"
primary={[m1, m2]} comparison={[m3, m4]} />
</div>✗ Don't
<WidgetTrackingItem title="x" sectionLabel="X" primary={[m1, m2]} comparison={[m3, m4]} style={{ width: 400 }} />WidgetTrackingItem is authored for the 4×4 quarter slot — 276 × 288 px. Other slot sizes are served by sibling Widget* components. Forcing a different width breaks the dashboard grid math and ships a visual misalignment.
<WidgetTrackingItem primary={[m1, m2] as any} comparison={[m3, m4, m5] as any} title="x" sectionLabel="X" />The grid is fixed at exactly 2×2 — four metric cells. The `primary` and `comparison` props are typed as `[T, T]` tuples to make this a compile-time error. If you need an N-cell metric grid, reach for a sibling tracker widget or compose multiple WidgetTrackingItem instances side-by-side.
<WidgetTrackingItem comparison={[{ label: 'Δ', caption: 'YoY', value: '-15%', trend: 'up' }, m4] as any} title="x" sectionLabel="X" primary={[m1, m2]} />`trend` is the visual signal of business-significance, not the sign of the number. Painting `-15%` as green up-trend is technically supported (e.g. cost reduction is "up" in a savings dashboard), but the consumer is responsible for keeping the tone aligned with the dashboard's domain semantics. Disagreement between sign and tone produces a visually confusing surface.
Design rationale
WidgetTrackingItem is the third member of the dashboard-widget family — a fixed-dim 276×288 surface authored for the same 4×4 quarter slot as WidgetLeaderboard / WidgetDonutChart, but tuned for at-a-glance "track this number" metric grids (avg price + delta + comparison + vs-PPI is the canonical Figma authoring). The 2×2 layout is locked at the type level via tuple props (`[T, T]`) so a misuse (3+ cells in a row) is a compile-time error. Outer dims do NOT scale with `size`; only typography density does. Trend tone is consumer-driven (`up` / `down` / `neutral`) — the widget paints the value + chevron in the matching semantic color (`--oshon-color-success-700` for up, `--oshon-color-error-700` for down) but does NOT infer trend from the value sign, because business-significance is a domain decision (cost reductions read as `up` in savings dashboards). Trend tones flow through `applyTheme()` so the deltas retint alongside the rest of the brand. Per-cell `onClick`, header `onTitleClick`, and the popover-anchored kebab Menu mirror WidgetLeaderboard exactly so dashboard widgets feel like a coherent family. Figma file `MsyCsSxIRkgRjWd1bSJpq6` node `12002:5481`.