Kosmesis
Components

Chart

A lightweight SVG bar/line chart with a ChartConfig CSS-variable convention for per-series colors. No Recharts dependency.

Installation

npx kosmesis add chart
pnpm dlx kosmesis add chart
yarn dlx kosmesis add chart
bunx kosmesis add chart

Copy and paste the following code into your project.

chart.tsx
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { cn } from "@/lib/utils";export interface ChartSeriesConfig {  label: string;  color: string;}export type ChartConfig = Record<string, ChartSeriesConfig>;export interface ChartContainerProps {  config: ChartConfig;  class?: string;  children?: Children;}// Ships a small SVG bar/line renderer instead of a React-only charting library — consumers who// bring their own SVG can still use ChartContainer/ChartTooltip/ChartLegend for theming.@Component()export class ChartContainer extends StatelessComponent<ChartContainerProps> {  render() {    const { config, class: cls, children } = this.props;    const vars = Object.fromEntries(      Object.entries(config).map(([key, value]) => [`--color-${key}`, value.color]),    );    return (      <div        data-slot="chart"        style={vars}        class={cn(          "flex aspect-video justify-center text-xs",          "[&_.chart-grid-line]:stroke-border [&_.chart-axis-label]:fill-muted-foreground",          cls,        )}      >        {children}      </div>    );  }}export interface ChartLegendProps {  config: ChartConfig;  class?: string;}@Component()export class ChartLegend extends StatelessComponent<ChartLegendProps> {  render() {    const { config, class: cls } = this.props;    return (      <div class={cn("flex flex-wrap items-center justify-center gap-4 pt-3", cls)}>        {Object.entries(config).map(([key, series]) => (          <div key={key} class="flex items-center gap-1.5">            <span class="size-2 shrink-0 rounded-[2px]" style={{ backgroundColor: `var(--color-${key})` }} />            <span class="text-muted-foreground">{series.label}</span>          </div>        ))}      </div>    );  }}export interface ChartTooltipProps {  label: string;  items: Array<{ key: string; value: string | number; color: string }>;  class?: string;}@Component()export class ChartTooltip extends StatelessComponent<ChartTooltipProps> {  render() {    const { label, items, class: cls } = this.props;    return (      <div class={cn("grid min-w-32 gap-1.5 rounded-lg border bg-background px-2.5 py-1.5 text-xs shadow-xl", cls)}>        <p class="font-medium">{label}</p>        {items.map((item) => (          <div key={item.key} class="flex items-center justify-between gap-4">            <span class="flex items-center gap-1.5 text-muted-foreground">              <span class="size-2 shrink-0 rounded-[2px]" style={{ backgroundColor: item.color }} />              {item.key}            </span>            <span class="font-mono font-medium tabular-nums">{item.value}</span>          </div>        ))}      </div>    );  }}export interface ChartDatum {  label: string;  [seriesKey: string]: string | number;}export interface SimpleBarChartProps {  data: ChartDatum[];  config: ChartConfig;  height?: number;  class?: string;}@Component()export class SimpleBarChart extends StatelessComponent<SimpleBarChartProps> {  render() {    const { data, config, height = 240, class: cls } = this.props;    const keys = Object.keys(config);    const max = Math.max(1, ...data.flatMap((d) => keys.map((k) => Number(d[k]) || 0)));    const width = 600;    const padding = 24;    const groupWidth = (width - padding * 2) / data.length;    const barWidth = (groupWidth * 0.7) / keys.length;    return (      <svg viewBox={`0 0 ${String(width)} ${String(height)}`} class={cn("w-full", cls)} role="img" aria-label="Bar chart">        <line x1={padding} y1={height - padding} x2={width - padding} y2={height - padding} class="chart-grid-line" stroke-width="1" />        {data.map((datum, i) => {          const groupX = padding + i * groupWidth + groupWidth * 0.15;          return (            <g key={datum.label}>              {keys.map((key, k) => {                const value = Number(datum[key]) || 0;                const barHeight = ((height - padding * 2) * value) / max;                return (                  <rect                    key={key}                    x={groupX + k * barWidth}                    y={height - padding - barHeight}                    width={barWidth * 0.85}                    height={barHeight}                    rx={2}                    fill={`var(--color-${key})`}                  />                );              })}              <text x={groupX + (barWidth * keys.length) / 2} y={height - padding + 14} class="chart-axis-label" text-anchor="middle" font-size="10">                {datum.label}              </text>            </g>          );        })}      </svg>    );  }}export interface SimpleLineChartProps {  data: ChartDatum[];  config: ChartConfig;  height?: number;  class?: string;}@Component()export class SimpleLineChart extends StatelessComponent<SimpleLineChartProps> {  render() {    const { data, config, height = 240, class: cls } = this.props;    const keys = Object.keys(config);    const max = Math.max(1, ...data.flatMap((d) => keys.map((k) => Number(d[k]) || 0)));    const width = 600;    const padding = 24;    const stepX = (width - padding * 2) / Math.max(1, data.length - 1);    return (      <svg viewBox={`0 0 ${String(width)} ${String(height)}`} class={cn("w-full", cls)} role="img" aria-label="Line chart">        <line x1={padding} y1={height - padding} x2={width - padding} y2={height - padding} class="chart-grid-line" stroke-width="1" />        {keys.map((key) => {          const points = data            .map((datum, i) => {              const value = Number(datum[key]) || 0;              const x = padding + i * stepX;              const y = height - padding - ((height - padding * 2) * value) / max;              return `${String(x)},${String(y)}`;            })            .join(" ");          return <polyline key={key} points={points} fill="none" stroke={`var(--color-${key})`} stroke-width="2" />;        })}        {data.map((datum, i) => (          <text key={datum.label} x={padding + i * stepX} y={height - padding + 14} class="chart-axis-label" text-anchor="middle" font-size="10">            {datum.label}          </text>        ))}      </svg>    );  }}

Install the following dependencies:

npm install @praxisjs/css
pnpm add @praxisjs/css
yarn add @praxisjs/css
bun add @praxisjs/css

Copy and paste the following code into your project.

chart.tsx
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);export interface ChartSeriesConfig {  label: string;  color: string;}export type ChartConfig = Record<string, ChartSeriesConfig>;class ChartStyles extends Stylesheet {  $container = this.css({ display: "flex", aspectRatio: "16 / 9", justifyContent: "center", fontSize: "0.75rem" })    .on("& .chart-grid-line", { stroke: t.border })    .on("& .chart-axis-label", { fill: t.mutedForeground });  $legend = this.css({ display: "flex", flexWrap: "wrap", alignItems: "center", justifyContent: "center", gap: "1rem", paddingTop: "0.75rem" });  $legendItem = this.css({ display: "flex", alignItems: "center", gap: "0.375rem" });  $legendSwatch = this.css({ width: "0.5rem", height: "0.5rem", flexShrink: 0, borderRadius: "2px" });  $legendLabel = this.css({ color: t.mutedForeground });  $tooltip = this.css({    display: "grid",    minWidth: "8rem",    gap: "0.375rem",    borderRadius: "0.5rem",    border: `1px solid ${t.border}`,    backgroundColor: t.background,    padding: "0.375rem 0.625rem",    fontSize: "0.75rem",    boxShadow: "0 20px 25px -5px rgb(0 0 0 / 0.1)",  });  $tooltipLabel = this.css({ fontWeight: 500 });  $tooltipRow = this.css({ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "1rem" });  $tooltipKey = this.css({ display: "flex", alignItems: "center", gap: "0.375rem", color: t.mutedForeground });  $tooltipValue = this.css({ fontFamily: "monospace", fontWeight: 500, fontVariantNumeric: "tabular-nums" });  $svg = this.css({ width: "100%" });}export interface ChartContainerProps {  config: ChartConfig;  class?: string;  children?: Children;}// Ships a small SVG bar/line renderer instead of a React-only charting library — consumers who// bring their own SVG can still use ChartContainer/ChartTooltip/ChartLegend for theming.@Component()export class ChartContainer extends StatelessComponent<ChartContainerProps> {  @Styled(ChartStyles) $s!: ChartStyles;  render() {    const { config, class: cls, children } = this.props;    const vars = Object.fromEntries(Object.entries(config).map(([key, value]) => [`--color-${key}`, value.color]));    return (      <div data-slot="chart" style={vars} class={cx(this.$s.$container, cls)}>        {children}      </div>    );  }}export interface ChartLegendProps {  config: ChartConfig;  class?: string;}@Component()export class ChartLegend extends StatelessComponent<ChartLegendProps> {  @Styled(ChartStyles) $s!: ChartStyles;  render() {    const { config, class: cls } = this.props;    return (      <div class={cx(this.$s.$legend, cls)}>        {Object.entries(config).map(([key, series]) => (          <div key={key} class={this.$s.$legendItem}>            <span class={this.$s.$legendSwatch} style={{ backgroundColor: `var(--color-${key})` }} />            <span class={this.$s.$legendLabel}>{series.label}</span>          </div>        ))}      </div>    );  }}export interface ChartTooltipProps {  label: string;  items: Array<{ key: string; value: string | number; color: string }>;  class?: string;}@Component()export class ChartTooltip extends StatelessComponent<ChartTooltipProps> {  @Styled(ChartStyles) $s!: ChartStyles;  render() {    const { label, items, class: cls } = this.props;    return (      <div class={cx(this.$s.$tooltip, cls)}>        <p class={this.$s.$tooltipLabel}>{label}</p>        {items.map((item) => (          <div key={item.key} class={this.$s.$tooltipRow}>            <span class={this.$s.$tooltipKey}>              <span class={this.$s.$legendSwatch} style={{ backgroundColor: item.color }} />              {item.key}            </span>            <span class={this.$s.$tooltipValue}>{item.value}</span>          </div>        ))}      </div>    );  }}export interface ChartDatum {  label: string;  [seriesKey: string]: string | number;}export interface SimpleBarChartProps {  data: ChartDatum[];  config: ChartConfig;  height?: number;  class?: string;}@Component()export class SimpleBarChart extends StatelessComponent<SimpleBarChartProps> {  @Styled(ChartStyles) $s!: ChartStyles;  render() {    const { data, config, height = 240, class: cls } = this.props;    const keys = Object.keys(config);    const max = Math.max(1, ...data.flatMap((d) => keys.map((k) => Number(d[k]) || 0)));    const width = 600;    const padding = 24;    const groupWidth = (width - padding * 2) / data.length;    const barWidth = (groupWidth * 0.7) / keys.length;    return (      <svg viewBox={`0 0 ${String(width)} ${String(height)}`} class={cx(this.$s.$svg, cls)} role="img" aria-label="Bar chart">        <line x1={padding} y1={height - padding} x2={width - padding} y2={height - padding} class="chart-grid-line" stroke-width="1" />        {data.map((datum, i) => {          const groupX = padding + i * groupWidth + groupWidth * 0.15;          return (            <g key={datum.label}>              {keys.map((key, k) => {                const value = Number(datum[key]) || 0;                const barHeight = ((height - padding * 2) * value) / max;                return (                  <rect                    key={key}                    x={groupX + k * barWidth}                    y={height - padding - barHeight}                    width={barWidth * 0.85}                    height={barHeight}                    rx={2}                    fill={`var(--color-${key})`}                  />                );              })}              <text x={groupX + (barWidth * keys.length) / 2} y={height - padding + 14} class="chart-axis-label" text-anchor="middle" font-size="10">                {datum.label}              </text>            </g>          );        })}      </svg>    );  }}export interface SimpleLineChartProps {  data: ChartDatum[];  config: ChartConfig;  height?: number;  class?: string;}@Component()export class SimpleLineChart extends StatelessComponent<SimpleLineChartProps> {  @Styled(ChartStyles) $s!: ChartStyles;  render() {    const { data, config, height = 240, class: cls } = this.props;    const keys = Object.keys(config);    const max = Math.max(1, ...data.flatMap((d) => keys.map((k) => Number(d[k]) || 0)));    const width = 600;    const padding = 24;    const stepX = (width - padding * 2) / Math.max(1, data.length - 1);    return (      <svg viewBox={`0 0 ${String(width)} ${String(height)}`} class={cx(this.$s.$svg, cls)} role="img" aria-label="Line chart">        <line x1={padding} y1={height - padding} x2={width - padding} y2={height - padding} class="chart-grid-line" stroke-width="1" />        {keys.map((key) => {          const points = data            .map((datum, i) => {              const value = Number(datum[key]) || 0;              const x = padding + i * stepX;              const y = height - padding - ((height - padding * 2) * value) / max;              return `${String(x)},${String(y)}`;            })            .join(" ");          return <polyline key={key} points={points} fill="none" stroke={`var(--color-${key})`} stroke-width="2" />;        })}        {data.map((datum, i) => (          <text key={datum.label} x={padding + i * stepX} y={height - padding + 14} class="chart-axis-label" text-anchor="middle" font-size="10">            {datum.label}          </text>        ))}      </svg>    );  }}

Examples

Usage

import { ChartContainer, ChartLegend, SimpleBarChart, type ChartConfig } from "@/components/ui/chart";

const config: ChartConfig = {
  desktop: { label: "Desktop", color: "var(--color-chart-1, #3b82f6)" },
  mobile: { label: "Mobile", color: "var(--color-chart-2, #10b981)" },
};

const data = [
  { label: "Jan", desktop: 186, mobile: 80 },
  { label: "Feb", desktop: 305, mobile: 200 },
  { label: "Mar", desktop: 237, mobile: 120 },
];

<ChartContainer config={config}>
  <SimpleBarChart data={data} config={config} />
</ChartContainer>
<ChartLegend config={config} />

Rather than pull in a chart library, this ships a small SVG bar/line renderer plus a ChartConfig --color-<key>-per-series convention, so ChartContainer/ChartTooltip/ChartLegend stay useful even if you bring your own SVG or another charting library instead of SimpleBarChart/SimpleLineChart.

Props

ChartContainer

PropTypeDefault
configChartConfig (Record<string, { label: string; color: string }>)— required
classstring

ChartLegend

PropTypeDefault
configChartConfig— required
classstring

ChartTooltip

PropTypeDefault
labelstring— required
itemsArray<{ key: string; value: string | number; color: string }>— required
classstring

SimpleBarChart / SimpleLineChart

PropTypeDefault
dataChartDatum[]— required
configChartConfig— required
heightnumber240
classstring

On this page