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 · Trend List

Wide 608×288 leaderboard — ranked list + 7-day sparkline · signed delta pills (success/error semantic palette) · null-safe gaps · drill-down row + title · applyTheme()-retinted trend line.

FigmaStorybookSource · PronpmPro

Unlock Widget · Trend List

Leaderboard + sparkline combo for top-N-with-trend dashboard headlines. 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
Top DistributorsLast 7 days
Data Name$ Name+/- Prev
Acme Co.$670K+10%
Globex$590K-4%
Initech$470K+8%
Umbrella$390K+11%
Wonka Industries$280K0%
MonTueWedThuFriSatSun
Top Distributors, Last 7 days. 5 items. Trend: Mon $194K, Tue $218K, Wed $256K, Thu $281K, Fri $312K, Sat $342K, Sun $368K.
Drill-down — clickable title + clickable rows
Title also wired
Data Name$ Name+/- Prev
MonTueWedThuFriSatSun
Click any row for distributor detail, Title also wired. 5 items. Trend: Mon $194K, Tue $218K, Wed $256K, Thu $281K, Fri $312K, Sat $342K, Sun $368K.
Drill-down trace
Click the title or any row to fire the drill-down handler.
Custom trend color — brand primary instead of tertiary
Active sessionslast 7 days
Data Name$ Name+/- Prev
iOS app162K+12%
Android app118K+8%
Web94K-2%
MonTueWedThuFriSatSun
Active sessions, last 7 days. 3 items. Trend: Mon 46K, Tue 51K, Wed 57K, Thu 61K, Fri 66K, Sat 69K, Sun 74K.
Null-safe gaps — missing days render as breaks
Pipeline valueWeekends omitted
Data Name$ Name+/- Prev
Total pipeline$4.2M+6%
Closed-won$1.8M+3%
MonTueWedThuFriSatSun
Pipeline value, Weekends omitted. 2 items. Trend: Mon $170K, Tue $200K, Fri $260K, Sat $295K, Sun $330K.
Kebab menu — popover-anchored, three actions
With a popover-anchored menu
Data Name$ Name+/- Prev
Acme Co.$670K+10%
Globex$590K-4%
Initech$470K+8%
MonTueWedThuFriSatSun
With a popover-anchored menu. 3 items. Trend: Mon $142K, Tue $159K, Wed $190K, Thu $203K, Fri $223K, Sat $246K, Sun $265K.
Menu trace
Click the ⋮ kebab to open the popover-anchored menu.

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 widgettrendlist

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·TrendList } from '@oshon-ai/components';

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

Leaderboard + sparkline combo for top-N-with-trend dashboard headlines. Part of the Data Visualization pack.

Unlock Widget · Trend List

Leaderboard + sparkline combo for top-N-with-trend dashboard headlines. 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·TrendList
  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 16, blue-stone-700, -0.32px tracking) + optional subtitle (Lato Regular 12, neutral-600) + trailing 20×20 kebab. Bottom edge carries a 1 px neutral-300 hairline separating header from content. Top corners radius 10 px.

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. When interactive, the kebab is wrapped in the Oshon `Tooltip` primitive (`moreLabel`, default `"More options"`) so hover/focus surfaces the Radix-backed dark chip; the tooltip dismisses automatically when the popover opens.

list-panel

Left-anchored 316 × 240 panel hosting the column headers + scrollable rows. Right edge carries a 1 px neutral-200 hairline separating it from the trend panel.

column-headers

24 px-tall header row — three columns at left:12 (Lato Medium 12, neutral-700): name (`nameLabel`, default `"Data Name"`, w:120, left-aligned), value (`valueLabel`, default `"$ Name"`, w:74, right-aligned), delta (`deltaLabel`, default `"+/- Prev"`, w:74, center-aligned). Bottom edge: 1 px neutral-200 hairline.

rows

Vertical-scrolling row container at top:24, bottom:0. Each row renders as a `<div>` (presentational) by default; becomes a `<button>` with hover background tint + focus ring + descriptive aria-label when `onItemClick` is supplied. Empty `items` → "No data" placeholder. The browser-native scrollbar sits at the right edge when content overflows the 216 px content band.

row

Single 40 px-tall row — name (Lato Medium 12, neutral-800, w:120, ellipsizes) at left:12, value (Lato Medium 12, neutral-800, w:74, right-aligned) at left:144, delta pill at right:23 (52 × 24, radius 4). Bottom edge: 1 px neutral-200 hairline. When `onItemClick` is supplied, the row `<button>` is wrapped in the Oshon `Tooltip` primitive so hover/focus surfaces a `"<Name>: <Value> (<Δ>)"` summary chip.

delta-pill

Signed-delta indicator — 52 × 24 px rounded-4 chip. Sign drives chrome through the semantic palette: positive ⇒ `--oshon-color-success-100` bg + `--oshon-color-success-700` text; negative ⇒ `--oshon-color-error-100` bg + `--oshon-color-error-700` text; zero ⇒ surface-muted bg + neutral text. `applyTheme()` retints all three states alongside the rest of the brand. Pill text flows through `formatDelta` (default emits a signed percentage: `10` → `"+10%"`, `-4` → `"-4%"`). Wrapped in the Oshon `Tooltip` primitive — hover/focus surfaces a `"<pillText> vs previous period"` chip — and stays keyboard-reachable (`tabIndex=0`) on the read-only row path; on the interactive-row path the pill drops out of the tab order (`tabIndex=-1`) so the row button is the single focus stop.

trend-panel

Right-anchored 292 × 240 panel hosting the y-axis labels, gridlines, sparkline, and day labels. Renders the chart in a single SVG overlay so polyline + dots stay vector-crisp at any zoom.

y-axis

5-tick numeric axis at left:12, vertical-centered on the matching gridline. Labels (Lato Medium 12, neutral-600, right-aligned in a 36 px column) format via `formatYTick` — default outputs compact currency (`200000` → `"$200K"`, `1500000` → `"$1.5M"`, `0` → `"0"`). The axis ceiling auto-rounds to a "nice" number via `niceCeiling()` when `yMax` is omitted; supply `yMax` explicitly to lock the axis.

gridline

5 horizontal hairlines (Oshon neutral-200) spanning x:56 → x:280 (within the trend panel), evenly spaced from CHART_TOP (y:20) to CHART_BOTTOM (y:204). The bottom gridline is the "0" baseline that the sparkline rests on; the top gridline marks `yMax`.

trend-line

SVG `<polyline>` connecting consecutive non-null sparkline points. Points with `value: null` (or `undefined`) break the line so missing-data gaps render as empty space rather than dropped-to-zero artifacts. Stroke flows through `trendColor` (default `var(--oshon-color-tertiary-700)` — the third seed of the brand palette so `applyTheme()` retints the trend line).

trend-dot

8 px (radius 4) SVG `<circle>` per non-null point. Fill matches `trendColor`. Hovering bumps r to 6 (120 ms ease) and surfaces an Oshon `Tooltip` (Radix-backed dark chip + arrow) — default text is `"<xLabel>: <formattedY>"` (e.g. `"Mon: $80K"`); customize via `formatPointTooltip(label, rawValue, formattedY)` or return `""` to suppress (returning `""` also removes the focus-trigger wrapper so a bare circle paints with no tab stop). Each dot is keyboard-reachable (`tabIndex=0`, `role="img"`, `aria-label=tooltipText`) so the tooltip opens on focus too. Up to 7 dots paint (one per visible column in the 7-day Figma anchor); points beyond the 7th are dropped.

day-label

X-axis category label below each column at top:212 (within the trend panel). Lato Medium 12, neutral-600, centered in the 30 px column. Label text comes from the `xLabels` prop (default `["Mon","Tue","Wed","Thu","Fri","Sat","Sun"]`); pass `formatXTick(label, index)` to abbreviate, localize, or re-case (e.g. `(s) => s.slice(0, 1)` to render `M T W T F S S`).

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 hover-underline link styling when `onTitleClick` is supplied. Each list row becomes a full-width `<button>` with hover background tint + focus ring + descriptive aria-label (`"<Name>, <Value>, <DeltaText>"`) when `onItemClick` is supplied. The sparkline carries `aria-hidden` and is paired with an SR-only summary listing the rendered points so screen readers get a scannable digest. List scrolls vertically when content overflows the 216 px content band. Tooltip surfaces flow through the Oshon `Tooltip` primitive (Radix-backed dark chip + arrow) on every tooltip-bearing slot — title, row, delta-pill, kebab, and trend-dot — so focusing a trigger opens the tooltip after a 300 ms delay and Escape dismisses it; native HTML `title=` and SVG `<title>` are deliberately NOT used.

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 — chart derives from items[].daily (sum across visible rows)
<WidgetTrendList
  title="Top Distributors"
  subtitle="Last 7 days"
  items={[
    { id: 'a', name: 'Acme Co.', value: '$670K', delta:  10, daily: [10000, 20000, 15000, 16000, null, null, null] },
    { id: 'b', name: 'Globex',   value: '$590K', delta:  -4, daily: [25000, 28000, 20000, 25000, null, null, null] },
    { id: 'c', name: 'Initech',  value: '$470K', delta:   8, daily: [30000, 25000, 20000, 20000, null, null, null] },
    { id: 'd', name: 'Umbrella', value: '$390K', delta:  11, daily: [15000, 20000, 15000, 15000, null, null, null] },
  ]}
  onMore={() => openMenu()}
/>
Drill-down — clickable title + clickable rows + Oshon Tooltip chip on every trigger
<WidgetTrendList
  title="Top Distributors"
  subtitle="Last 7 days"
  items={DISTRIBUTORS_WITH_DAILY}
  onTitleClick={() => router.push('/dashboards/distributors')}
  onItemClick={(item) => openDetail(item.id)}
/>
Custom x-axis labels + per-tick formatter (single-letter abbreviation)
<WidgetTrendList
  title="Hours by Team"
  items={TEAMS_WITH_DAILY}
  xLabels={["Mon","Tue","Wed","Thu","Fri","Sat","Sun"]}
  formatXTick={(label) => label.slice(0, 1)}
/>
Custom y-axis ceiling + tick formatter + dot tooltip
<WidgetTrendList
  title="Hours by Day"
  items={TEAMS_WITH_DAILY}
  yMax={40}
  yTickCount={5}
  formatYTick={(v) => v === 0 ? '0' : `${v}h`}
  formatPointTooltip={(label, _, fy) => `${label} → ${fy}`}
  trendColor="var(--oshon-color-primary-700)"
/>

✗ Don't

Hardcoded dimension override
<WidgetTrendList title="x" items={[]} trendData={[]} style={{ width: 800, height: 400 }} />

WidgetTrendList is authored for the wide half-row slot — 608 × 288 px. Other slot sizes are served by sibling Widget* components. Forcing different outer dims breaks the dashboard grid math and ships a visual misalignment.

Overlong trend series breaking the 7-column footprint
<WidgetTrendList title="x" items={ROWS} trendData={[/* 14 days */]} />

The trend plot area is 210 px wide with 30 px column-stride — 7 columns is the canonical Figma anchor. Beyond that the rightmost columns clip the panel edge. For wider time-series, reach for WidgetGroupedBarChart (no list, full-width 7-col plot) or chunk the data into multiple widgets stacked horizontally.

Mixing positive + negative trend values (no negative axis support)
<WidgetTrendList title="x" items={ROWS} trendData={[{ id: 'mon', label: 'Mon', value: -5000 }]} />

The Figma authoring is a positive-only axis with the 0-baseline at CHART_BOTTOM. Negative values clamp to 0 in `plotPoint()`. For diverging-axis charts (e.g. revenue vs cost), build a sibling widget that handles the symmetric layout explicitly, or normalize the trend data to a positive series before passing it.

Design rationale

WidgetTrendList is the sixth member of the dashboard-widget family — a fixed-dim 608×288 surface authored for the wide half-row slot, same outer footprint as WidgetGroupedBarChart but with a different internal split: list-on-the-left + sparkline-on-the-right. It is tuned for "rank a list of N entities AND show how the aggregate metric trended over the last 7 days" — the canonical top-distributors / top-products dashboard headline. The title paints in the brand primary so `applyTheme()` retints the headline, matching the rest of the wide-row family. The list scrolls vertically when content exceeds the 216 px content band so consumers can pass arbitrary-length item arrays without breaking the surface. Delta pills paint through the semantic success/error palette (positive ⇒ `--oshon-color-success-100/700`, negative ⇒ `--oshon-color-error-100/700`); the trend line defaults to the third seed of the brand palette (`--oshon-color-tertiary-700`) so the sparkline retints alongside the brand. The sparkline auto-rounds to a "nice" y-axis ceiling (`niceCeiling()`) so default axes read $200K instead of $187,432; consumers lock the axis with explicit `yMax`. Polyline breaks across `value: null` so missing-data gaps render as empty space rather than dropped-to-zero artifacts. `onTitleClick`, `onItemClick`, and the popover-anchored kebab Menu mirror sibling widgets exactly. Figma file `MsyCsSxIRkgRjWd1bSJpq6` node `12002:6161`.