Kosmesis
Components

Chart

A lightweight SVG bar/line chart with the shadcn-style ChartConfig CSS-variable convention. 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;}/** * Purely presentational — no Morphos equivalent. shadcn/ui's `Chart` wraps Recharts (a React-only * dependency); rather than pull in an equivalent library, this ships a small SVG bar/line * renderer (`BarChart`/`LineChart` below) plus the same `ChartContainer`/`ChartConfig` * CSS-variable convention, so consumers who bring their own SVG (or another charting lib) can * still use `ChartContainer`/`ChartTooltip`/`ChartLegend` for consistent 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;}/** Minimal grouped-bar SVG chart. Bring your own for anything more advanced. */@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;}/** Minimal multi-series line SVG chart. Bring your own for anything more advanced. */@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;}/** * Purely presentational — no Morphos equivalent. shadcn/ui's `Chart` wraps Recharts (a React-only * dependency); rather than pull in an equivalent library, this ships a small SVG bar/line * renderer (`SimpleBarChart`/`SimpleLineChart` below) plus the same `ChartConfig` * `--color-<key>`-per-series CSS-variable convention, so `ChartContainer`/`ChartTooltip`/ * `ChartLegend` stay useful even if you bring your own SVG or another charting library. */@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;}/** Minimal grouped-bar SVG chart. Bring your own for anything more advanced. */@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;}/** Minimal multi-series line SVG chart. Bring your own for anything more advanced. */@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} />

shadcn/ui's Chart wraps Recharts, a React-only dependency. Rather than pull in an equivalent library, this ships a small SVG bar/line renderer plus the same 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.

On this page