ComponentsChart

Chart

component

Line / 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; }