ComponentsChart
Chart
componentLine / area / bar / stacked / donut on the kit’s derived 6-series palette.
Traffic by source
One ChartFrame, six render modes — gridlines, axes, hover tooltips.
Best practices
Do
- Series colours come from the derived --k-chart-1..6 palette — every kit ships a harmonised set that survives re-theming.
- Give every chart an insight header (title + the delta that matters), not just axes.
- Use the recipe's legend and keep axis labels in caption type.
Don't
- Don't introduce colours outside the chart palette — they break theming and dark mode.
- Don't plot more than six series in one frame; split the story instead.
SessionsSignups
Composes: Card
Best practices
Do
- Series colours come from the derived --k-chart-1..6 palette — every kit ships a harmonised set that survives re-theming.
- Give every chart an insight header (title + the delta that matters), not just axes.
- Use the recipe's legend and keep axis labels in caption type.
Don't
- Don't introduce colours outside the chart palette — they break theming and dark mode.
- Don't plot more than six series in one frame; split the story instead.
Recipe CSS
This is the exact CSS your kit ships for Chart — token-driven, so it re-themes with every knob. Get it (and the rest) from the configurator's Use kit panel.
/* === Chart ===
Data-viz elements (bars, sparklines) deliberately do NOT scale with the
theme's radius — a pill-shaped bar reads as a pill, not as data. A small
fixed radius keeps the chart legible across all 5 radius choices.
The accent color (not primary) sits here on purpose: a chart is where
the user's brand accent earns its keep in a SaaS dashboard.
Bar sizing follows shadcn/Recharts convention: bars 16-30px with ~2:1
bar-to-gap ratio. flex:1 gave fat bars filling the row — slimmer + more
breathing room reads as data rather than blocks. */
.barchart {
display: flex;
align-items: flex-end;
gap: var(--k-s-8);
height: 80px;
}
.barchart__bar {
flex: 1 1 0;
min-width: 14px;
max-width: 30px;
position: relative;
background: var(--k-accent);
/* Bar-top follows the system radius but stays CAPPED: a chart bar should
* read square-ish, never a pill. Couples to the (scaling) inner radius so a
* square Style → square tops, but min()-clamps at 4px so round Styles don't
* give lozenge-topped bars (bad charting practice). Same idiom as
* --k-row-radius. Was a hardcoded 3px, decoupled from the Radius control. */
border-radius: min(4px, var(--k-radius-sm)) min(4px, var(--k-radius-sm)) 0 0;
opacity: 0.9;
transform-origin: bottom;
/* Easing follows the theme's --k-ease-out so Playful gives the bars a soft overshoot too */
animation: bar-rise 700ms var(--k-ease-out, cubic-bezier(.2, .8, .2, 1)) backwards;
/* Hover lifts the bar to full opacity — pairs with the value tooltip below. */
transition: opacity var(--k-dur-fast, 120ms) var(--k-ease, ease);
}
@keyframes bar-rise { from { transform: scaleY(0); } to { transform: scaleY(1); } }
/* Hover/focus value tooltip — CSS-only so it survives the framework-neutral
export (no JS runtime); same dark-pill look as .tt__pop. Bars carry
tabindex + aria-label, so the global :focus-visible ring plus this tip make
the data keyboard-reachable, not just hover-only. */
.barchart__bar:hover,
.barchart__bar:focus-visible { opacity: 1; }
.barchart__tip {
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%) translateY(3px);
background: var(--k-fg);
color: var(--k-bg);
font-size: var(--k-type-eyebrow);
font-weight: var(--k-weight-semibold);
line-height: 1;
padding-block: var(--k-s-4);
padding-inline: var(--k-s-6);
border-radius: var(--k-radius-sm);
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity var(--k-dur-fast, 120ms) var(--k-ease, ease),
transform var(--k-dur-fast, 120ms) var(--k-ease, ease);
z-index: var(--k-z-tooltip);
}
.barchart__bar:hover .barchart__tip,
.barchart__bar:focus-visible .barchart__tip {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* Stagger up to 7 bars — covers gallery card (7) and dashboard 7-day chart */
.barchart__bar:nth-child(1) { animation-delay: 0ms; }
.barchart__bar:nth-child(2) { animation-delay: 60ms; }
.barchart__bar:nth-child(3) { animation-delay: 120ms; }
.barchart__bar:nth-child(4) { animation-delay: 180ms; }
.barchart__bar:nth-child(5) { animation-delay: 240ms; }
.barchart__bar:nth-child(6) { animation-delay: 300ms; }
.barchart__bar:nth-child(7) { animation-delay: 360ms; }
/* Categorical variant — each bar is a distinct series, colored from the
* derived chart palette (chart-1..6). For by-category breakdowns; the default
* single-series time chart above stays one accent color. */
.barchart--series .barchart__bar { opacity: 1; }
.barchart--series .barchart__bar:nth-child(1) { background: var(--k-chart-1); }
.barchart--series .barchart__bar:nth-child(2) { background: var(--k-chart-2); }
.barchart--series .barchart__bar:nth-child(3) { background: var(--k-chart-3); }
.barchart--series .barchart__bar:nth-child(4) { background: var(--k-chart-4); }
.barchart--series .barchart__bar:nth-child(5) { background: var(--k-chart-5); }
.barchart--series .barchart__bar:nth-child(6) { background: var(--k-chart-6); }
/* Legend — series swatch + label row under a categorical chart */
.chart-legend { display: flex; flex-wrap: wrap; gap: var(--k-s-6) var(--k-s-12); margin-top: var(--k-s-12); }
.chart-legend__item { display: inline-flex; align-items: center; gap: var(--k-s-4); font-size: var(--k-type-eyebrow); color: var(--k-fg-muted); }
.chart-legend__dot { width: var(--k-marker); height: var(--k-marker); border-radius: 2px; flex: none; }
/* ChartFrame — presentational chart family on the --k-chart-1..6 palette.
* One container, five render modes (line / area / bar / stacked / donut).
* The SVG fills its frame; series colours come straight from the palette. */
.chart { display: flex; flex-direction: column; gap: var(--k-s-2); }
.chart__canvas { width: 100%; position: relative; }
/* No-data state — a centred placeholder filling the plot area, not a blank box. */
.chart__empty {
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: var(--k-s-8); padding: var(--k-s-16);
color: var(--k-fg-faint); font-size: var(--k-type-small); text-align: center;
}
.chart__empty svg { width: 1.75rem; height: 1.75rem; opacity: 0.55; }
/* Loading state — skeleton bars (reuse .sk shimmer) while data fetches. */
.chart__loading { display: flex; align-items: flex-end; gap: var(--k-s-8); padding: var(--k-s-8) 0; }
.chart__loading-bar { flex: 1; min-width: 0; border-radius: var(--k-radius-sm) var(--k-radius-sm) 0 0; }
.chart__svg { width: 100%; height: 100%; display: block; }
.chart__svg--donut { max-width: 220px; margin: 0 auto; }
.chart__axis { stroke: var(--k-border); stroke-width: 1; vector-effect: non-scaling-stroke; }
.chart__donut-num { font-size: var(--k-type-h3); font-weight: var(--k-weight-bold); fill: var(--k-fg); font-family: var(--k-font-display); font-variant-numeric: tabular-nums; }
.chart__donut-cap { font-size: var(--k-type-caption); fill: var(--k-fg-faint); text-transform: uppercase; letter-spacing: var(--k-track-eyebrow); }
.chart__legend { display: flex; flex-wrap: wrap; gap: var(--k-s-6) var(--k-s-12); margin-top: var(--k-s-10); }
.chart__legend-item { display: inline-flex; align-items: center; gap: var(--k-s-4); font-size: var(--k-type-eyebrow); color: var(--k-fg-muted); }
.chart__swatch { width: var(--k-marker); height: var(--k-marker); border-radius: 2px; flex: none; }
/* Gridlines — faint horizontal guides behind the marks. Baseline reuses
* .chart__axis (stronger). Both are non-scaling so they stay 1px under the
* stretched (preserveAspectRatio:none) viewBox. */
.chart__grid { stroke: var(--k-border); stroke-width: 1; opacity: 0.5; vector-effect: non-scaling-stroke; }
/* y-axis ticks — crisp HTML overlay (SVG text would stretch). Sits in the
* top-left, three labels spread top→bottom matching the gridlines. */
.chart__yaxis { position: absolute; inset: 0 auto 0 0; display: flex; flex-direction: column; justify-content: space-between; padding: 0 var(--k-s-4) 0 0; pointer-events: none; z-index: 1; }
.chart__ytick { font-size: var(--k-type-caption); color: var(--k-fg-faint); font-variant-numeric: tabular-nums; line-height: 1; background: var(--k-surface); padding: 0 var(--k-s-2) 0 0; border-radius: 2px; }
/* x-axis labels — evenly distributed under the canvas. */
.chart__xlabels { display: flex; justify-content: space-between; margin-top: var(--k-s-4); }
.chart__xlabel { font-size: var(--k-type-caption); color: var(--k-fg-faint); font-variant-numeric: tabular-nums; transition: color var(--k-dur-fast, 140ms) var(--k-ease, ease); }
.chart__xlabel--on { color: var(--k-fg); font-weight: var(--k-weight-semibold); }
/* Tracking cursor — vertical hairline at the hovered column. */
.chart__cursor { stroke: var(--k-fg-faint); stroke-width: 1; stroke-dasharray: 3 3; vector-effect: non-scaling-stroke; opacity: 0.7; pointer-events: none; }
/* Hover tooltip — floats above the hovered column, centred + clamped inside. */
.chart__tip { position: absolute; top: var(--k-s-2); transform: translateX(-50%); min-width: 116px; max-width: 180px; padding: var(--k-s-6) var(--k-s-8); background: var(--k-surface-overlay, var(--k-surface-raised)); border: var(--k-hairline, 1px solid var(--k-border)); border-radius: var(--k-radius-sm); box-shadow: var(--k-shadow-md); font-size: var(--k-type-caption); pointer-events: none; z-index: 3; }
.chart__tip-label { font-weight: var(--k-weight-semibold); color: var(--k-fg); margin-bottom: var(--k-s-4); }
.chart__tip-row { display: flex; align-items: center; gap: var(--k-s-4); line-height: 1.5; }
.chart__tip-dot { width: var(--k-marker); height: var(--k-marker); border-radius: 2px; flex: none; }
.chart__tip-name { color: var(--k-fg-muted); }
.chart__tip-val { margin-left: auto; font-weight: var(--k-weight-semibold); color: var(--k-fg); font-variant-numeric: tabular-nums; }
/* ----- TreeView (.tree) -----
* Disclosure tree — rows with a rotating chevron; nested .tree__group is
* indented with a guide-line. Selected row follows the selection accent. */
.tree { font-size: var(--k-type-small); }
.tree__row { display: flex; align-items: center; gap: var(--k-s-6); min-height: var(--k-row-h-sm, 28px); padding: 0 var(--k-s-8); border-radius: var(--k-radius-sm, 6px); cursor: pointer; color: var(--k-fg-muted); }
.tree__row:hover { background: var(--k-state-hover); color: var(--k-fg); }
.tree__row--on { background: var(--k-state-selected-bg, var(--k-primary-soft)); color: var(--k-state-selected-fg, var(--k-primary)); font-weight: var(--k-weight-semibold); }
.tree__chev { display: inline-flex; color: var(--k-fg-faint); transition: transform var(--k-dur-fast, 140ms) var(--k-ease, ease); flex: none; }
.tree__chev--leaf { visibility: hidden; }
.tree__row[aria-expanded='true'] .tree__chev { transform: rotate(90deg); }
.tree__icon { display: inline-flex; color: var(--k-fg-faint); flex: none; }
.tree__group { margin-left: var(--k-s-12); border-left: var(--k-divider); padding-left: var(--k-s-6); }
/* ----- StatusPage (.statuspage) -----
* Service status board — overall banner + per-service 90-day uptime ticks.
* Tick tone uses the semantic colours (success / warning / danger). */
.statuspage { display: flex; flex-direction: column; gap: var(--k-s-12); }
.statuspage__banner { display: flex; align-items: center; gap: var(--k-s-8); padding: var(--k-s-10) var(--k-s-12); border-radius: var(--k-radius-md); background: var(--k-success-soft); color: var(--k-success-soft-fg); font-weight: var(--k-weight-semibold); font-size: var(--k-type-small); }
.statuspage__row { display: flex; align-items: center; gap: var(--k-s-10); }
.statuspage__name { font-size: var(--k-type-small); width: 116px; flex: none; }
.statuspage__bars { display: flex; gap: var(--k-s-2); flex: 1; min-width: 0; }
.statuspage__tick { flex: 1; height: 22px; border-radius: 2px; background: var(--k-success); }
.statuspage__tick--warn { background: var(--k-warning); }
.statuspage__tick--down { background: var(--k-danger); }
.statuspage__pct { font-size: var(--k-type-eyebrow); color: var(--k-fg-muted); width: 46px; text-align: right; flex: none; }
/* NotificationCenter — now just the LIST system: `.list .list--flush` rows with
* `.list__lead--icon-muted` + `.list__title--lg` + `.list__sub` + a trailing
* `.list__dot` (unread). No separate .notif-item/.notif-center family. */
/* ----- Kanban (.kanban) -----
* Presentational board — equal-width sunken columns with a header count and
* stacked draggable-looking cards. (No DnD — the layout is the deliverable.) */
/* Columns keep a 220px floor and the board scrolls horizontally rather than
* crushing 4 columns into a phone width (the standard kanban-on-mobile pattern);
* on wide screens 1fr lets them share the space. */
.kanban { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(min(220px, 100%), 1fr); gap: var(--k-s-10); overflow-x: auto; }
.kanban__col { background: var(--k-surface-sunken); border-radius: var(--k-radius-md); padding: var(--k-stack-gap, 8px); display: flex; flex-direction: column; gap: var(--k-stack-gap, 8px); }
.kanban__col-head { display: flex; align-items: center; justify-content: space-between; font-size: var(--k-type-eyebrow); font-weight: var(--k-weight-semibold); color: var(--k-fg-muted); padding: var(--k-s-2) var(--k-s-4); }
.kanban__count { background: var(--k-surface); border-radius: 999px; padding: 1px var(--k-s-6); font-size: var(--k-type-caption); }
/* Real <button> issue card — focusable + keyboard-activatable. The resets make
* it read like the old div; hover/active use the shared discrete-card system
* (border + lift) so it matches .stat-tile--clickable / .quickact__tile. */
.kanban__card { width: 100%; text-align: left; font-family: inherit; color: inherit; appearance: none; -webkit-appearance: none; background: var(--k-surface); border: 1px solid var(--k-border); border-radius: var(--k-radius-sm, 6px); padding: var(--k-space, 16px); font-size: var(--k-type-small); display: flex; flex-direction: column; gap: var(--k-stack-gap, 8px); box-shadow: var(--k-shadow-sm); cursor: pointer; transition: border-color var(--k-dur-fast, 140ms) var(--k-ease, ease), box-shadow var(--k-dur-fast, 140ms) var(--k-ease, ease), transform var(--k-dur-fast, 140ms) var(--k-ease, ease); }
.kanban__card:hover { border-color: var(--k-state-border, var(--k-border)); box-shadow: var(--k-shadow-md); transform: translateY(-1px); }
.kanban__card:active { transform: translateY(0); }
/* Title is CONTENT, so it rides --k-type-body (scales with the Text-size
* control: 14→16 S→XL) — NOT --k-type-small, which is reserved for the
* meta/labels below it. The card base stays small so key/points/tag default
* small; only the title is lifted to the content tier. */
.kanban__card-title { font-size: var(--k-type-body); font-weight: var(--k-weight-medium); line-height: 1.35; }
.kanban__card-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--k-s-8); }
/* Rich board-card parts (issue-tracker style): epic tag, issue key + type
* glyph, story-point pill, inline meta glyphs, assignee avatar. */
/* Epic/category tag — the ACCENT container (H4 usage pass): a tag is quiet
metadata, not an action, so it wears accent-soft instead of shouting in
solid primary (consumers were overriding it with hardcoded hexes — gone). */
.kanban__tag { align-self: flex-start; max-width: 100%; font-size: var(--k-type-caption); font-weight: var(--k-weight-bold); letter-spacing: var(--k-track-eyebrow); text-transform: uppercase; padding: var(--k-s-2) var(--k-s-6); border-radius: var(--k-radius-sm, 4px); background: var(--k-accent-soft); color: var(--k-accent-soft-fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.kanban__stats { display: inline-flex; align-items: center; gap: var(--k-s-6); min-width: 0; }
.kanban__key { display: inline-flex; align-items: center; gap: var(--k-s-4); font-size: var(--k-type-eyebrow); font-weight: var(--k-weight-semibold); color: var(--k-fg-muted); white-space: nowrap; }
.kanban__key svg, .kanban__icons svg { width: var(--k-icon-sm); height: var(--k-icon-sm); flex: none; }
.kanban__pts { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; padding: 0 var(--k-s-4); border-radius: 999px; background: var(--k-surface-sunken); font-size: var(--k-type-caption); font-weight: var(--k-weight-semibold); color: var(--k-fg-muted); }
.kanban__icons { display: inline-flex; align-items: center; gap: var(--k-s-6); color: var(--k-fg-faint); }
.kanban__prio { display: inline-flex; flex-direction: column; gap: 1.5px; width: 11px; color: var(--k-fg-faint); }
.kanban__prio i { height: 1.5px; border-radius: 1px; background: currentColor; }