Skip to content

Chart

Beautiful charts. Built using LayerChart. Copy and paste into your apps.

Bar Chart - Interactive

Showing total visitors for the last 3 months

<script lang="ts">
  import * as Chart from "$lib/components/ui/chart/index.js";
  import * as Card from "$lib/components/ui/card/index.js";
  import { scaleUtc } from "d3-scale";
  import { BarChart, Highlight } from "layerchart";
  import { cubicInOut } from "svelte/easing";
 
  const chartData = [
    { date: new Date("2024-04-01"), desktop: 222, mobile: 150 },
    { date: new Date("2024-04-02"), desktop: 97, mobile: 180 },
    { date: new Date("2024-04-03"), desktop: 167, mobile: 120 },
    { date: new Date("2024-04-04"), desktop: 242, mobile: 260 },
    { date: new Date("2024-04-05"), desktop: 373, mobile: 290 },
    { date: new Date("2024-04-06"), desktop: 301, mobile: 340 },
    { date: new Date("2024-04-07"), desktop: 245, mobile: 180 },
    { date: new Date("2024-04-08"), desktop: 409, mobile: 320 },
    { date: new Date("2024-04-09"), desktop: 59, mobile: 110 },
    { date: new Date("2024-04-10"), desktop: 261, mobile: 190 },
    { date: new Date("2024-04-11"), desktop: 327, mobile: 350 },
    { date: new Date("2024-04-12"), desktop: 292, mobile: 210 },
    { date: new Date("2024-04-13"), desktop: 342, mobile: 380 },
    { date: new Date("2024-04-14"), desktop: 137, mobile: 220 },
    { date: new Date("2024-04-15"), desktop: 120, mobile: 170 },
    { date: new Date("2024-04-16"), desktop: 138, mobile: 190 },
    { date: new Date("2024-04-17"), desktop: 446, mobile: 360 },
    { date: new Date("2024-04-18"), desktop: 364, mobile: 410 },
    { date: new Date("2024-04-19"), desktop: 243, mobile: 180 },
    { date: new Date("2024-04-20"), desktop: 89, mobile: 150 },
    { date: new Date("2024-04-21"), desktop: 137, mobile: 200 },
    { date: new Date("2024-04-22"), desktop: 224, mobile: 170 },
    { date: new Date("2024-04-23"), desktop: 138, mobile: 230 },
    { date: new Date("2024-04-24"), desktop: 387, mobile: 290 },
    { date: new Date("2024-04-25"), desktop: 215, mobile: 250 },
    { date: new Date("2024-04-26"), desktop: 75, mobile: 130 },
    { date: new Date("2024-04-27"), desktop: 383, mobile: 420 },
    { date: new Date("2024-04-28"), desktop: 122, mobile: 180 },
    { date: new Date("2024-04-29"), desktop: 315, mobile: 240 },
    { date: new Date("2024-04-30"), desktop: 454, mobile: 380 },
    { date: new Date("2024-05-01"), desktop: 165, mobile: 220 },
    { date: new Date("2024-05-02"), desktop: 293, mobile: 310 },
    { date: new Date("2024-05-03"), desktop: 247, mobile: 190 },
    { date: new Date("2024-05-04"), desktop: 385, mobile: 420 },
    { date: new Date("2024-05-05"), desktop: 481, mobile: 390 },
    { date: new Date("2024-05-06"), desktop: 498, mobile: 520 },
    { date: new Date("2024-05-07"), desktop: 388, mobile: 300 },
    { date: new Date("2024-05-08"), desktop: 149, mobile: 210 },
    { date: new Date("2024-05-09"), desktop: 227, mobile: 180 },
    { date: new Date("2024-05-10"), desktop: 293, mobile: 330 },
    { date: new Date("2024-05-11"), desktop: 335, mobile: 270 },
    { date: new Date("2024-05-12"), desktop: 197, mobile: 240 },
    { date: new Date("2024-05-13"), desktop: 197, mobile: 160 },
    { date: new Date("2024-05-14"), desktop: 448, mobile: 490 },
    { date: new Date("2024-05-15"), desktop: 473, mobile: 380 },
    { date: new Date("2024-05-16"), desktop: 338, mobile: 400 },
    { date: new Date("2024-05-17"), desktop: 499, mobile: 420 },
    { date: new Date("2024-05-18"), desktop: 315, mobile: 350 },
    { date: new Date("2024-05-19"), desktop: 235, mobile: 180 },
    { date: new Date("2024-05-20"), desktop: 177, mobile: 230 },
    { date: new Date("2024-05-21"), desktop: 82, mobile: 140 },
    { date: new Date("2024-05-22"), desktop: 81, mobile: 120 },
    { date: new Date("2024-05-23"), desktop: 252, mobile: 290 },
    { date: new Date("2024-05-24"), desktop: 294, mobile: 220 },
    { date: new Date("2024-05-25"), desktop: 201, mobile: 250 },
    { date: new Date("2024-05-26"), desktop: 213, mobile: 170 },
    { date: new Date("2024-05-27"), desktop: 420, mobile: 460 },
    { date: new Date("2024-05-28"), desktop: 233, mobile: 190 },
    { date: new Date("2024-05-29"), desktop: 78, mobile: 130 },
    { date: new Date("2024-05-30"), desktop: 340, mobile: 280 },
    { date: new Date("2024-05-31"), desktop: 178, mobile: 230 },
    { date: new Date("2024-06-01"), desktop: 178, mobile: 200 },
    { date: new Date("2024-06-02"), desktop: 470, mobile: 410 },
    { date: new Date("2024-06-03"), desktop: 103, mobile: 160 },
    { date: new Date("2024-06-04"), desktop: 439, mobile: 380 },
    { date: new Date("2024-06-05"), desktop: 88, mobile: 140 },
    { date: new Date("2024-06-06"), desktop: 294, mobile: 250 },
    { date: new Date("2024-06-07"), desktop: 323, mobile: 370 },
    { date: new Date("2024-06-08"), desktop: 385, mobile: 320 },
    { date: new Date("2024-06-09"), desktop: 438, mobile: 480 },
    { date: new Date("2024-06-10"), desktop: 155, mobile: 200 },
    { date: new Date("2024-06-11"), desktop: 92, mobile: 150 },
    { date: new Date("2024-06-12"), desktop: 492, mobile: 420 },
    { date: new Date("2024-06-13"), desktop: 81, mobile: 130 },
    { date: new Date("2024-06-14"), desktop: 426, mobile: 380 },
    { date: new Date("2024-06-15"), desktop: 307, mobile: 350 },
    { date: new Date("2024-06-16"), desktop: 371, mobile: 310 },
    { date: new Date("2024-06-17"), desktop: 475, mobile: 520 },
    { date: new Date("2024-06-18"), desktop: 107, mobile: 170 },
    { date: new Date("2024-06-19"), desktop: 341, mobile: 290 },
    { date: new Date("2024-06-20"), desktop: 408, mobile: 450 },
    { date: new Date("2024-06-21"), desktop: 169, mobile: 210 },
    { date: new Date("2024-06-22"), desktop: 317, mobile: 270 },
    { date: new Date("2024-06-23"), desktop: 480, mobile: 530 },
    { date: new Date("2024-06-24"), desktop: 132, mobile: 180 },
    { date: new Date("2024-06-25"), desktop: 141, mobile: 190 },
    { date: new Date("2024-06-26"), desktop: 434, mobile: 380 },
    { date: new Date("2024-06-27"), desktop: 448, mobile: 490 },
    { date: new Date("2024-06-28"), desktop: 149, mobile: 200 },
    { date: new Date("2024-06-29"), desktop: 103, mobile: 160 },
    { date: new Date("2024-06-30"), desktop: 446, mobile: 400 }
  ];
 
  const chartConfig = {
    views: { label: "Page Views", color: "" },
    desktop: { label: "Desktop", color: "var(--chart-2)" },
    mobile: { label: "Mobile", color: "var(--chart-1)" }
  } satisfies Chart.ChartConfig;
 
  let activeChart = $state<keyof typeof chartConfig>("desktop");
 
  const total = $derived({
    desktop: chartData.reduce((acc, curr) => acc + curr.desktop, 0),
    mobile: chartData.reduce((acc, curr) => acc + curr.mobile, 0)
  });
 
  const activeSeries = $derived([
    {
      key: activeChart,
      label: chartConfig[activeChart].label,
      color: chartConfig[activeChart].color
    }
  ]);
</script>
 
<Card.Root>
  <Card.Header
    class="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row"
  >
    <div class="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
      <Card.Title>Bar Chart - Interactive</Card.Title>
      <Card.Description
        >Showing total visitors for the last 3 months</Card.Description
      >
    </div>
    <div class="flex">
      {#each ["desktop", "mobile"] as key (key)}
        {@const chart = key as keyof typeof chartConfig}
        <button
          data-active={activeChart === chart}
          class="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-start even:border-s sm:border-s sm:border-t-0 sm:px-8 sm:py-6"
          onclick={() => (activeChart = chart)}
        >
          <span class="text-muted-foreground text-xs">
            {chartConfig[chart].label}
          </span>
          <span class="text-lg leading-none font-bold sm:text-3xl">
            {total[key as keyof typeof total].toLocaleString()}
          </span>
        </button>
      {/each}
    </div>
  </Card.Header>
  <Card.Content class="px-2 sm:p-6">
    <Chart.Container config={chartConfig} class="aspect-auto h-[250px] w-full">
      <BarChart
        data={chartData}
        x="date"
        axis="x"
        series={activeSeries}
        props={{
          bars: {
            stroke: "none",
            rounded: "none",
            motion: { type: "tween", duration: 500, easing: cubicInOut }
          },
          highlight: { area: { fill: "none" } },
          xAxis: {
            format: (d: Date) => {
              return d.toLocaleDateString("en-US", {
                month: "short",
                day: "2-digit"
              });
            },
            ticks: (scale) => scaleUtc(scale.domain(), scale.range()).ticks()
          }
        }}
      >
        {#snippet belowMarks()}
          <Highlight area={{ class: "fill-muted" }} />
        {/snippet}
        {#snippet tooltip()}
          <Chart.Tooltip
            nameKey="views"
            labelFormatter={(v: Date) => {
              return v.toLocaleDateString("en-US", {
                month: "short",
                day: "numeric",
                year: "numeric"
              });
            }}
          />
        {/snippet}
      </BarChart>
    </Chart.Container>
  </Card.Content>
</Card.Root>

Introducing Charts. A collection of chart components that you can copy and paste into your apps.

Charts are designed to look great out of the box. They work well with other components are are fully customizable to fit your project.

Component

We use LayerChart under the hood.

We designed the Chart component with composition in mind. You build your charts using LayerChart components and only bring in custom components, such as ChartTooltip, when and where you need it

<script lang="ts">
  import * as Chart from '$lib/components/ui/chart/index.js';
  import { BarChart } from 'layerchart';
 
  const data = [
    // ...
  ];
</script>
 
<Chart.Container>
  <BarChart {data} x="date" y="value">
    {#snippet tooltip()}
      <Chart.Tooltip />
    {/snippet}
  </BarChart>
</Chart.Container>

We do not wrap LayerChart. This means you're not locked into an abstraction. When a new LayerChart version is released, you can follow the official upgrade path to upgrade your charts.

The components are yours.

Installation

pnpm dlx shadcn-svelte@latest add chart

Your First Chart

Let's build your first chart. We'll build a bar chart with an axis, grid, tooltip, and legend.

Start by defining your data

The following data represents the number of desktop and mobile users for each month.

lib/components/example-chart.svelte
<script lang="ts">
  const chartData = [
    { month: 'January', desktop: 186, mobile: 80 },
    { month: 'February', desktop: 305, mobile: 200 },
    { month: 'March', desktop: 237, mobile: 120 },
    { month: 'April', desktop: 73, mobile: 190 },
    { month: 'May', desktop: 209, mobile: 130 },
    { month: 'June', desktop: 214, mobile: 140 }
  ];
</script>

Define your chart config

The chart config holds configuration for the chart. This is where you place human-readable strings, such as labels, icons, and color tokens for theming.

lib/components/example-chart.svelte
<script lang="ts">
  import * as Chart from '$lib/components/ui/chart/index.js';
 
  const chartConfig = {
    desktop: {
      label: 'Desktop',
      color: '#2563eb'
    },
    mobile: {
      label: 'Mobile',
      color: '#60a5fa'
    }
  } satisfies Chart.ChartConfig;
</script>

Build your chart

You can now build your chart using LayerChart components. We're using the BarChart component in this example, which is one of LayerChart's "Simplified Charts".

These components handle a lot of the common chart scaffolding for you, while allowing you to customize them to your liking.

chart/chart-container.svelte
<script lang="ts">
  import { cn, type WithElementRef } from '$UTILS$.js';
  import type { HTMLAttributes } from 'svelte/elements';
  import ChartStyle from './chart-style.svelte';
  import { setChartContext, type ChartConfig } from './chart-utils.js';

  const uid = $props.id();

  let {
    ref = $bindable(null),
    id = uid,
    class: className,
    children,
    config,
    ...restProps
  }: WithElementRef<HTMLAttributes<HTMLElement>> & {
    config: ChartConfig;
  } = $props();

  const chartId = $derived(`chart-${id || uid.replace(/:/g, '')}`);

  setChartContext({
    get config() {
      return config;
    }
  });
</script>

<div
  bind:this={ref}
  data-chart={chartId}
  data-slot="chart"
  class={cn(
    'flex aspect-video justify-center overflow-visible border border-zinc-800 bg-background p-3 font-mono text-xs text-zinc-400',
    // Overrides
    //
    // Stroke around dots/marks when hovering
    '[&_.lc-highlight-point]:stroke-transparent',
    // override the default stroke color of lines
    '[&_.lc-line]:stroke-zinc-700',

    // by default, layerchart shows a line intersecting the point when hovering, this hides that
    '[&_.lc-highlight-line]:stroke-0',

    // by default, when you hover a point on a stacked series chart, it will drop the opacity
    // of the other series, this overrides that
    '[&_.lc-area-path]:opacity-100 [&_.lc-highlight-line]:opacity-100 [&_.lc-highlight-point]:opacity-100 [&_.lc-spline-path]:opacity-100 [&_.lc-text]:text-xs [&_.lc-text-svg]:overflow-visible',

    // We don't want the little tick lines between the axis labels and the chart, so we remove
    // the stroke. The alternative is to manually disable `tickMarks` on the x/y axis of every
    // chart.
    '[&_.lc-axis-tick]:stroke-0',

    // We don't want to display the rule on the x/y axis, as there is already going to be
    // a grid line there and rule ends up overlapping the marks because it is rendered after
    // the marks
    '[&_.lc-rule-x-line:not(.lc-grid-x-rule)]:stroke-0 [&_.lc-rule-y-line:not(.lc-grid-y-rule)]:stroke-0',
    '[&_.lc-grid-x-radial-line]:stroke-zinc-800 [&_.lc-grid-x-radial-circle]:stroke-zinc-800',
    '[&_.lc-grid-y-radial-line]:stroke-zinc-800 [&_.lc-grid-y-radial-circle]:stroke-zinc-800',

    // Legend adjustments
    '[&_.lc-legend-swatch-button]:items-center [&_.lc-legend-swatch-button]:gap-1.5',
    '[&_.lc-legend-swatch-group]:items-center [&_.lc-legend-swatch-group]:gap-4',
    '[&_.lc-legend-swatch]:size-2.5 [&_.lc-legend-swatch]:rounded-[2px]',

    // Labels
    '[&_.lc-labels-text:not([fill])]:fill-zinc-100 [&_text]:stroke-transparent',

    // Tick labels on th x/y axes
    '[&_.lc-axis-tick-label]:fill-zinc-500 [&_.lc-axis-tick-label]:font-mono [&_.lc-axis-tick-label]:font-medium',
    '[&_.lc-tooltip-rects-g]:fill-transparent',
    '[&_.lc-layout-svg-g]:fill-transparent',
    '[&_.lc-root-container]:w-full',
    className
  )}
  {...restProps}
>
  <ChartStyle id={chartId} {config} />
  {@render children?.()}
</div>
chart/chart-style.svelte
<script lang="ts">
  import { THEMES, type ChartConfig } from './chart-utils.js';

  let { id, config }: { id: string; config: ChartConfig } = $props();

  const colorConfig = $derived(
    config ? Object.entries(config).filter(([, config]) => config.theme || config.color) : null
  );

  const themeContents = $derived.by(() => {
    if (!colorConfig || !colorConfig.length) return;

    const themeContents = [];
    for (const [_theme, prefix] of Object.entries(THEMES)) {
      let content = `${prefix} [data-chart=${id}] {\n`;
      const color = colorConfig.map(([key, itemConfig]) => {
        const theme = _theme as keyof typeof itemConfig.theme;
        const color = itemConfig.theme?.[theme] || itemConfig.color;
        return color ? `\t--color-${key}: ${color};` : null;
      });

      content += color.join('\n') + '\n}';

      themeContents.push(content);
    }

    return themeContents.join('\n');
  });
</script>

{#if themeContents}
  {#key id}
    <svelte:element this={"style"}>
      {themeContents}
    </svelte:element>
  {/key}
{/if}
chart/chart-tooltip.svelte
<script lang="ts">
  import { cn, type WithElementRef, type WithoutChildren } from '$UTILS$.js';
  import type { HTMLAttributes } from 'svelte/elements';
  import { getPayloadConfigFromPayload, useChart, type TooltipPayload } from './chart-utils.js';
  import { getChartContext, Tooltip as TooltipPrimitive } from 'layerchart';
  import type { Snippet } from 'svelte';

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  function defaultFormatter(value: any) {
    return `${value}`;
  }

  let {
    ref = $bindable(null),
    class: className,
    hideLabel = false,
    indicator = 'dot',
    hideIndicator = false,
    labelKey,
    label,
    labelFormatter = defaultFormatter,
    labelClassName,
    formatter,
    nameKey,
    color,
    ...restProps
  }: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> & {
    hideLabel?: boolean;
    label?: string;
    indicator?: 'line' | 'dot' | 'dashed';
    nameKey?: string;
    labelKey?: string;
    hideIndicator?: boolean;
    labelClassName?: string;
    labelFormatter?: // eslint-disable-next-line @typescript-eslint/no-explicit-any
      ((value: any, payload: TooltipPayload[]) => string | number | Snippet) | null;
    formatter?: Snippet<
      [
        {
          value: unknown;
          name: string;
          item: TooltipPayload;
          index: number;
          payload: TooltipPayload[];
        }
      ]
    >;
  } = $props();

  const chart = useChart();
  const chartCtx = getChartContext();

  // Filter to series with defined values (important for item-based charts like Pie/Arc
  // where only the hovered item has a value)
  const visibleSeries = $derived(
    chartCtx.tooltip.series.filter((s: TooltipPayload) => s.value !== undefined)
  );

  const formattedLabel = $derived.by(() => {
    if (hideLabel || !visibleSeries?.length) return null;

    const [item] = visibleSeries;
    const tooltipData = chartCtx.tooltip.data;

    // Get the x-axis label value from the raw tooltip data (e.g. a Date or month string)
    const dataLabel = tooltipData != null ? chartCtx.x(tooltipData) : undefined;

    const key = labelKey ?? item?.label ?? item?.key ?? 'value';
    const itemConfig = getPayloadConfigFromPayload(
      chart.config,
      item,
      key,
      tooltipData as Record<string, unknown> | null
    );

    let value: unknown;
    if (!labelKey && typeof label === 'string') {
      value = chart.config[label as keyof typeof chart.config]?.label ?? label;
    } else if (labelKey) {
      value = itemConfig?.label ?? dataLabel;
    } else {
      value = dataLabel;
    }

    if (value === undefined) return null;
    if (!labelFormatter) return value;
    return labelFormatter(value, visibleSeries);
  });

  const nestLabel = $derived(visibleSeries.length === 1 && indicator !== 'dot');
</script>

{#snippet TooltipLabel()}
  {#if formattedLabel}
    <div class={cn('font-medium', labelClassName)}>
      {#if typeof formattedLabel === 'function'}
        {@render formattedLabel()}
      {:else}
        {formattedLabel}
      {/if}
    </div>
  {/if}
{/snippet}

<TooltipPrimitive.Root variant="none">
  <div
    bind:this={ref}
    class={cn(
      'grid min-w-[9rem] items-start gap-1.5 border border-zinc-800 bg-zinc-950 px-2.5 py-1.5 font-mono text-xs text-zinc-100 shadow-none',
      className
    )}
    {...restProps}
  >
    {#if !nestLabel}
      {@render TooltipLabel()}
    {/if}
    <div class="grid gap-1.5">
      {#each visibleSeries as item, i (item.key + i)}
        {@const key = `${nameKey || item.key || item.label || 'value'}`}
        {@const itemConfig = getPayloadConfigFromPayload(
          chart.config,
          item,
          key,
          chartCtx.tooltip.data
        )}
        {@const indicatorColor = color || item.config?.color || item.color}
        <div
          class={cn(
            'flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5 [&>svg]:text-zinc-500',
            indicator === 'dot' && 'items-center'
          )}
        >
          {#if formatter && item.value !== undefined && item.label}
            {@render formatter({
              value: item.value,
              name: item.label,
              item,
              index: i,
              payload: visibleSeries
            })}
          {:else}
            {#if itemConfig?.icon}
              <itemConfig.icon />
            {:else if !hideIndicator}
              <div
                style="--color-bg: {indicatorColor}; --color-border: {indicatorColor};"
                class={cn('shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)', {
                  'size-2.5': indicator === 'dot',
                  'h-full w-1': indicator === 'line',
                  'w-0 border-[1.5px] border-dashed bg-transparent': indicator === 'dashed',
                  'my-0.5': nestLabel && indicator === 'dashed'
                })}
              ></div>
            {/if}
            <div
              class={cn(
                'flex flex-1 shrink-0 justify-between leading-none',
                nestLabel ? 'items-end' : 'items-center'
              )}
            >
              <div class="grid gap-1.5">
                {#if nestLabel}
                  {@render TooltipLabel()}
                {/if}
                <span class="text-zinc-500">
                  {itemConfig?.label || item.label}
                </span>
              </div>
              {#if item.value !== undefined}
                <span class="font-mono font-semibold text-zinc-100 tabular-nums">
                  {item.value.toLocaleString()}
                </span>
              {/if}
            </div>
          {/if}
        </div>
      {/each}
    </div>
  </div>
</TooltipPrimitive.Root>
chart/chart-utils.ts
import type { Tooltip } from 'layerchart';
import { getContext, setContext, type Component, type Snippet } from 'svelte';

export const THEMES = { light: '', dark: '.dark' } as const;

export type ChartConfig = {
  [k in string]: {
    label?: string;
    icon?: Component;
  } & (
    | { color?: string; theme?: never }
    | { color?: never; theme: Record<keyof typeof THEMES, string> }
  );
};

export type ExtractSnippetParams<T> = T extends Snippet<[infer P]> ? P : never;

export type TooltipPayload = Tooltip.TooltipSeries;

// Helper to extract item config from a payload.
export function getPayloadConfigFromPayload(
  config: ChartConfig,
  payload: TooltipPayload,
  key: string,
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  data?: Record<string, any> | null
) {
  if (typeof payload !== 'object' || payload === null) return undefined;

  const payloadConfig =
    'config' in payload && typeof payload.config === 'object' && payload.config !== null
      ? payload.config
      : undefined;

  let configLabelKey: string = key;

  if (payload.key === key) {
    configLabelKey = payload.key;
  } else if (payload.label === key) {
    configLabelKey = payload.label;
  } else if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {
    configLabelKey = payload[key as keyof typeof payload] as string;
  } else if (
    payloadConfig !== undefined &&
    key in payloadConfig &&
    typeof payloadConfig[key as keyof typeof payloadConfig] === 'string'
  ) {
    configLabelKey = payloadConfig[key as keyof typeof payloadConfig] as string;
  } else if (data != null && key in data && typeof data[key] === 'string') {
    configLabelKey = data[key] as string;
  }

  return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
}

type ChartContextValue = {
  config: ChartConfig;
};

const chartContextKey = Symbol('chart-context');

export function setChartContext(value: ChartContextValue) {
  return setContext(chartContextKey, value);
}

export function useChart() {
  return getContext<ChartContextValue>(chartContextKey);
}
chart/index.ts
import ChartContainer from './chart-container.svelte';
import ChartTooltip from './chart-tooltip.svelte';

export { getPayloadConfigFromPayload, type ChartConfig } from './chart-utils.js';

export { ChartContainer, ChartTooltip, ChartContainer as Container, ChartTooltip as Tooltip };
<script lang="ts">
  import * as Chart from "$lib/components/ui/chart/index.js";
  import { scaleBand } from "d3-scale";
  import { BarChart } from "layerchart";
 
  const chartData = [
    { month: "January", desktop: 186, mobile: 80 },
    { month: "February", desktop: 305, mobile: 200 },
    { month: "March", desktop: 237, mobile: 120 },
    { month: "April", desktop: 73, mobile: 190 },
    { month: "May", desktop: 209, mobile: 130 },
    { month: "June", desktop: 214, mobile: 140 }
  ];
 
  const chartConfig = {
    desktop: {
      label: "Desktop",
      color: "#2563eb"
    },
    mobile: {
      label: "Mobile",
      color: "#60a5fa"
    }
  } satisfies Chart.ChartConfig;
</script>
 
<Chart.Container config={chartConfig} class="min-h-[200px] w-full">
  <BarChart
    data={chartData}
    xScale={scaleBand().padding(0.25)}
    x="month"
    axis="x"
    seriesLayout="group"
    tooltipContext={false}
    series={[
      {
        key: "desktop",
        label: chartConfig.desktop.label,
        color: chartConfig.desktop.color
      },
      {
        key: "mobile",
        label: chartConfig.mobile.label,
        color: chartConfig.mobile.color
      }
    ]}
  />
</Chart.Container>

We now have a group-stacked bar chart with an x axis and a grid.

Adjusting the Axis Ticks

Our bar chart is currently displaying the full month name for each tick on the x axis. Let's shorten it to just the first three letters.

Add a custom formatter to the x axis

The props prop is how you can pass custom props to the various components that make up the chart. Here we're passing a custom formatter to the x axis.

<Chart.Container config={chartConfig} class="min-h-[200px] w-full">
  <BarChart
    data={chartData}
    xScale={scaleBand().padding(0.25)}
    x="month"
    axis="x"
    tooltipContext={false}
    seriesLayout="group"
    series={[
      {
        key: 'desktop',
        label: chartConfig.desktop.label,
        color: chartConfig.desktop.color
      },
      {
        key: 'mobile',
        label: chartConfig.mobile.label,
        color: chartConfig.mobile.color
      }
    ]}
    props={{
      xAxis: {
        format: (d) => d.slice(0, 3)
      }
    }}
  />
</Chart.Container>
<script lang="ts">
  import * as Chart from "$lib/components/ui/chart/index.js";
  import { scaleBand } from "d3-scale";
  import { BarChart } from "layerchart";
 
  const chartData = [
    { month: "January", desktop: 186, mobile: 80 },
    { month: "February", desktop: 305, mobile: 200 },
    { month: "March", desktop: 237, mobile: 120 },
    { month: "April", desktop: 73, mobile: 190 },
    { month: "May", desktop: 209, mobile: 130 },
    { month: "June", desktop: 214, mobile: 140 }
  ];
 
  const chartConfig = {
    desktop: {
      label: "Desktop",
      color: "#2563eb"
    },
    mobile: {
      label: "Mobile",
      color: "#60a5fa"
    }
  } satisfies Chart.ChartConfig;
</script>
 
<Chart.Container config={chartConfig} class="min-h-[200px] w-full">
  <BarChart
    data={chartData}
    xScale={scaleBand().padding(0.25)}
    x="month"
    axis="x"
    tooltipContext={false}
    seriesLayout="group"
    series={[
      {
        key: "desktop",
        label: chartConfig.desktop.label,
        color: chartConfig.desktop.color
      },
      {
        key: "mobile",
        label: chartConfig.mobile.label,
        color: chartConfig.mobile.color
      }
    ]}
    props={{
      xAxis: {
        format: (d) => d.slice(0, 3)
      }
    }}
  />
</Chart.Container>

Add Tooltip

So far we've only used the BarChart component from LayerChart. They look great out of the box thanks to some customizations in the chart component.

To add a tooltip, we'll use the custom Chart.Tooltip component from chart.

Add the Chart.Tooltip component to the chart

We'll replace the tooltipContext={false} prop with the tooltip snippet where we'll place the Chart.Tooltip component.

<Chart.Container config={chartConfig} class="min-h-[200px] w-full">
  <BarChart
    data={chartData}
    xScale={scaleBand().padding(0.25)}
    x="month"
    axis="x"
    seriesLayout="group"
    series={[
      {
        key: 'desktop',
        label: chartConfig.desktop.label,
        color: chartConfig.desktop.color
      },
      {
        key: 'mobile',
        label: chartConfig.mobile.label,
        color: chartConfig.mobile.color
      }
    ]}
    props={{
      xAxis: {
        format: (d) => d.slice(0, 3)
      }
    }}
  >
    {#snippet tooltip()}
      <Chart.Tooltip />
    {/snippet}
  </BarChart>
</Chart.Container>
<script lang="ts">
  import * as Chart from "$lib/components/ui/chart/index.js";
  import { scaleBand } from "d3-scale";
  import { BarChart } from "layerchart";
 
  const chartData = [
    { month: "January", desktop: 186, mobile: 80 },
    { month: "February", desktop: 305, mobile: 200 },
    { month: "March", desktop: 237, mobile: 120 },
    { month: "April", desktop: 73, mobile: 190 },
    { month: "May", desktop: 209, mobile: 130 },
    { month: "June", desktop: 214, mobile: 140 }
  ];
 
  const chartConfig = {
    desktop: {
      label: "Desktop",
      color: "#2563eb"
    },
    mobile: {
      label: "Mobile",
      color: "#60a5fa"
    }
  } satisfies Chart.ChartConfig;
</script>
 
<Chart.Container config={chartConfig} class="min-h-[200px] w-full">
  <BarChart
    data={chartData}
    xScale={scaleBand().padding(0.25)}
    x="month"
    axis="x"
    seriesLayout="group"
    series={[
      {
        key: "desktop",
        label: chartConfig.desktop.label,
        color: chartConfig.desktop.color
      },
      {
        key: "mobile",
        label: chartConfig.mobile.label,
        color: chartConfig.mobile.color
      }
    ]}
  >
    {#snippet tooltip()}
      <Chart.Tooltip />
    {/snippet}
  </BarChart>
</Chart.Container>

Add Legend

Set the legend prop to true

The legend prop is used to show a legend for the chart. We are working with LayerChart to add a payload similar to the tooltip so we can more easily create a custom legend.

<Chart.Container config={chartConfig} class="min-h-[200px] w-full">
  <BarChart
    data={chartData}
    xScale={scaleBand().padding(0.25)}
    x="month"
    axis="x"
    seriesLayout="group"
    legend
    series={[
      {
        key: 'desktop',
        label: chartConfig.desktop.label,
        color: chartConfig.desktop.color
      },
      {
        key: 'mobile',
        label: chartConfig.mobile.label,
        color: chartConfig.mobile.color
      }
    ]}
    props={{
      xAxis: {
        format: (d) => d.slice(0, 3)
      }
    }}
  >
    {#snippet tooltip()}
      <Chart.Tooltip />
    {/snippet}
  </BarChart>
</Chart.Container>
<script lang="ts">
  import * as Chart from "$lib/components/ui/chart/index.js";
  import { scaleBand } from "d3-scale";
  import { BarChart } from "layerchart";
 
  const chartData = [
    { month: "January", desktop: 186, mobile: 80 },
    { month: "February", desktop: 305, mobile: 200 },
    { month: "March", desktop: 237, mobile: 120 },
    { month: "April", desktop: 73, mobile: 190 },
    { month: "May", desktop: 209, mobile: 130 },
    { month: "June", desktop: 214, mobile: 140 }
  ];
 
  const chartConfig = {
    desktop: {
      label: "Desktop",
      color: "#2563eb"
    },
    mobile: {
      label: "Mobile",
      color: "#60a5fa"
    }
  } satisfies Chart.ChartConfig;
</script>
 
<Chart.Container config={chartConfig} class="min-h-[200px] w-full">
  <BarChart
    data={chartData}
    xScale={scaleBand().padding(0.25)}
    x="month"
    axis="x"
    seriesLayout="group"
    legend
    series={[
      {
        key: "desktop",
        label: chartConfig.desktop.label,
        color: chartConfig.desktop.color
      },
      {
        key: "mobile",
        label: chartConfig.mobile.label,
        color: chartConfig.mobile.color
      }
    ]}
  >
    {#snippet tooltip()}
      <Chart.Tooltip />
    {/snippet}
  </BarChart>
</Chart.Container>

Done. You've built your first chart! What's next?

Chart Config

The chart config is where you define the labels, icons and colors for a chart.

It is intentionally decoupled from chart data.

This allows you to share config and color tokens between charts. It can also works independently for cases where your data or color tokens live remotely or in a different format.

<script lang="ts">
  import MonitorIcon from '@lucide/svelte/icons/monitor';
  import * as Chart from '$lib/components/ui/chart/index.js';
 
  const chartConfig = {
    desktop: {
      label: 'Desktop',
      icon: MonitorIcon,
      // A color like 'hsl(220, 98%, 61%)' or 'var(--color-name)'
      color: '#2563eb',
      // OR a theme object with 'light' and 'dark' keys
      theme: {
        light: '#2563eb',
        dark: '#dc2626'
      }
    }
  } satisfies Chart.ChartConfig;
</script>

Theming

Charts has built-in support for theming. You can use css variables (recommended) or color values in any color format, such as hex, hsl, or oklch.

CSS Variables

Define your colors in your css file

src/routes/layout.css
:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  /* ... */
  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
}
 
.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  /* ... */
  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
}

Add the color to your chartConfig

<script lang="ts">
  const chartConfig = {
    desktop: {
      label: 'Desktop',
      color: 'var(--chart-1)'
    },
    mobile: {
      label: 'Mobile',
      color: 'var(--chart-2)'
    }
  } satisfies Chart.ChartConfig;
</script>

hex, hsl or oklch

You can also define your colors directly in the chart config. Use the color format you prefer.

<script lang="ts">
  const chartConfig = {
    desktop: {
      label: 'Desktop',
      color: '#2563eb'
    }
  } satisfies Chart.ChartConfig;
</script>

Using Colors

To use the theme colors in your chart, reference the colors using the format var(--color-KEY).

Components

<Bar fill="var(--color-desktop)" />

Chart Data

const chartData = [
  { browser: 'chrome', visitors: 275, color: 'var(--color-chrome)' },
  { browser: 'safari', visitors: 200, color: 'var(--color-safari)' }
];

Tailwind

<Label class="fill-(--color-desktop)" />

Tooltip

A chart tooltip contains a label, name, indicator and value. You can use a combination of these to customize your tooltip.

Label
Page Views
Desktop
186
Mobile
80
Name
Chrome
1,286
Firefox
1,000
Page Views
Desktop
12,486
Indicator
Chrome
1,286
<script lang="ts">
  import TooltipDemo from "$lib/components/chart-tooltip-demo-item.svelte";
</script>
 
<div
  class="text-foreground grid aspect-video w-full max-w-md justify-center md:grid-cols-2 [&>div]:relative [&>div]:flex [&>div]:h-[137px] [&>div]:w-[224px] [&>div]:items-center [&>div]:justify-center [&>div]:p-4"
>
  <div>
    <div class="absolute start-[-35px] top-[45px] z-10 text-sm font-medium">
      Label
    </div>
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 193 40"
      width="50"
      height="12"
      fill="none"
      class="absolute start-[5px] top-[50px] z-10"
    >
      <g clip-path="url(#a)">
        <path
          fill="currentColor"
          d="M173.928 21.13C115.811 44.938 58.751 45.773 0 26.141c4.227-4.386 7.82-2.715 10.567-1.88 21.133 5.64 42.9 6.266 64.457 7.101 31.066 1.253 60.441-5.848 89.183-17.335 1.268-.418 2.325-1.253 4.861-2.924-14.582-2.924-29.165 2.089-41.845-3.76.212-.835.212-1.879.423-2.714 9.51-.627 19.231-1.253 28.742-2.089 9.51-.835 18.808-1.88 28.318-2.506 6.974-.418 9.933 2.924 7.397 9.19-3.17 8.145-7.608 15.664-11.623 23.391-.423.836-1.057 1.88-1.902 2.298-2.325.835-4.65 1.044-7.186 1.67-.422-2.088-1.479-4.386-1.268-6.265.423-2.506 1.902-4.595 3.804-9.19Z"
        />
      </g>
      <defs>
        <clipPath id="a">
          <path fill="currentColor" d="M0 0h193v40H0z" />
        </clipPath>
      </defs>
    </svg>
    <TooltipDemo
      label="Page Views"
      payload={[
        { name: "Desktop", value: 186, color: "var(--chart-1)" },
        { name: "Mobile", value: 80, color: "var(--chart-2)" }
      ]}
      class="w-[8rem]"
    />
  </div>
  <div class="items-end">
    <div class="absolute start-[122px] top-[0px] z-10 text-sm font-medium">
      Name
    </div>
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width="35"
      height="42"
      fill="none"
      viewBox="0 0 122 148"
      class="absolute start-[85px] top-[10px] z-10 -scale-x-100"
    >
      <g clip-path="url(#ab)">
        <path
          fill="currentColor"
          d="M0 2.65c6.15-4.024 12.299-2.753 17.812-.847a115.56 115.56 0 0 1 21.84 10.59C70.4 32.727 88.849 61.744 96.483 97.54c1.908 9.108 2.544 18.639 3.817 29.017 8.481-4.871 12.934-14.402 21.416-19.909 1.061 4.236-1.06 6.989-2.756 9.319-6.998 9.531-14.207 19.062-21.63 28.382-3.604 4.448-6.36 4.871-10.177 1.059-8.058-7.837-12.935-17.368-14.42-28.382 0-.424.636-1.059 1.485-2.118 9.118 2.33 6.997 13.979 14.843 18.215 3.393-14.614.848-28.593-2.969-42.149-4.029-14.19-9.33-27.746-17.812-39.82-8.27-11.86-18.66-21.392-30.11-30.287C26.93 11.758 14.207 6.039 0 2.65Z"
        />
      </g>
      <defs>
        <clipPath id="ab">
          <path fill="currentColor" d="M0 0h122v148H0z" />
        </clipPath>
      </defs>
    </svg>
    <TooltipDemo
      label="Browser"
      hideLabel
      payload={[
        { name: "Chrome", value: 1286, color: "var(--chart-3)" },
        { name: "Firefox", value: 1000, color: "var(--chart-4)" }
      ]}
      indicator="dashed"
      class="w-[8rem]"
    />
  </div>
  <div class="!hidden md:!flex">
    <TooltipDemo
      label="Page Views"
      payload={[{ name: "Desktop", value: 12486, color: "var(--chart-3)" }]}
      class="w-[9rem]"
      indicator="line"
    />
  </div>
  <div class="!items-start !justify-start">
    <div class="absolute start-[50px] top-[60px] z-10 text-sm font-medium">
      Indicator
    </div>
    <TooltipDemo
      label="Browser"
      hideLabel
      payload={[{ name: "Chrome", value: 1286, color: "var(--chart-1)" }]}
      indicator="dot"
      class="w-[8rem]"
    />
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width="15"
      height="34"
      fill="none"
      viewBox="0 0 75 175"
      class="absolute start-[30px] top-[38px] z-10 rotate-[-40deg]"
    >
      <g clip-path="url(#abc)">
        <path
          fill="currentColor"
          d="M20.187 175c-4.439-2.109-7.186-2.531-8.032-4.008-3.17-5.484-6.763-10.968-8.454-17.084-5.073-16.242-4.439-32.694-1.057-49.146 5.707-28.053 18.388-52.942 34.24-76.565 1.692-2.531 3.171-5.063 4.862-7.805 0-.21-.211-.632-.634-1.265-4.65 1.265-9.511 2.53-14.161 3.585-2.537.422-5.496.422-8.032-.421-1.48-.422-3.593-2.742-3.593-4.219 0-1.898 1.48-4.218 2.747-5.906 1.057-1.054 2.96-1.265 4.65-1.687C35.406 7.315 48.088 3.729 60.98.776c10.99-2.53 14.584 1.055 13.95 11.812-.634 11.18-.846 22.358-1.268 33.326-.212 3.375-.846 6.96-1.268 10.757-8.878-4.007-8.878-4.007-12.048-38.177C47.03 33.259 38.153 49.289 29.91 65.741 21.667 82.193 16.17 99.49 13.212 117.84c-2.959 18.984.634 36.912 6.975 57.161Z"
        />
      </g>
      <defs>
        <clipPath id="abc">
          <path fill="currentColor" d="M0 0h75v175H0z" />
        </clipPath>
      </defs>
    </svg>
  </div>
</div>

You can turn on/off any of these using the hideLabel, hideIndicator props and customize the indicator style using the indicator prop.

Use labelKey and nameKey to use a custom key for the tooltip label and name.

Chart comes with the <Chart.Tooltip> component. You can use this component to add custom tooltips to your chart.

Props

Use the following props to customize the tooltip.

Prop Type Description
labelKey string The config or data key to use for the label.
nameKey string The config or data key to use for the name.
indicator dot line or dashed The indicator style for the tooltip.
hideLabel boolean Whether to hide the label.
hideIndicator boolean Whether to hide the indicator.
label string A custom label for the tooltip
labelFormatter function A function to format the label.
formatter Snippet A snippet to provide flexible rendering of the tooltip.

Colors

Colors are automatically referenced from the chart config.

Custom

To use a custom key for tooltip label and names, use the labelKey and nameKey props.

<script lang="ts">
  const chartData = [
    { browser: 'chrome', visitors: 187, color: 'var(--color-chrome)' },
    { browser: 'safari', visitors: 200, color: 'var(--color-safari)' }
  ];
 
  const chartConfig = {
    visitors: {
      label: 'Total Visitors'
    },
    chrome: {
      label: 'Chrome',
      color: 'var(--chart-1)'
    },
    safari: {
      label: 'Safari',
      color: 'var(--chart-2)'
    }
  } satisfies ChartConfig;
</script>
 
<Chart.Tooltip labelKey="visitors" nameKey="browser" />

This will use Total Visitors for label and Chrome and Safari for the tooltip names.