🍡 mochi
← All demos

Charts with LayerChart

LayerChart renders to plain HTML or SVG, so it server-renders like any other Svelte component. The first chart ships no JavaScript; the rest are islands.

Minimal setup: install it, import from layerchart, and Mochi bundles it into whichever island imports it. Full component reference at layerchart.com.

bun add layerchart

Pure SSR — SVG

LayerChart can render to pure SVG or HTML by omitting Mochi's hydrate directives. That needs two opt-ins: ssr (LayerChart skips server rendering otherwise) and explicit width/height to set the dimensions, since nothing resizes the container client side.

SVG is recommended for pure SSR graphs: the <Svg> gets a matching viewBox, so the fixed 600×220 geometry rescales to any column width with CSS alone.

It's composed from <Chart> primitives rather than the <BarChart> shortcut: that shortcut's marks snippet shadows its own marks prop, which the Svelte server compiler turns into unbounded recursion once ssr forces a server render.

<script lang="ts">
  import { Chart, Svg, Axis, Bars } from 'layerchart/svg';
  import { traffic } from './data.ts';
</script>

<Chart ssr data={traffic} x="month" y="requests" c="requests" cRange={['var(--chart-1)']} width={600} height={220}>
  <Svg viewBox="0 0 600 220">
    <Axis placement="left" grid rule ticks={4} />
    <Axis placement="bottom" rule />
    <Bars fill="var(--chart-1)" />
  </Svg>
</Chart>
0k 1k 2k 3k Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec

Theming — light/dark

LayerChart ships its tooltip, axis, and other default styles pre-coloured with CSS light-dark() so they read correctly in both modes out of the box — you lean on that any time you use a LayerChart component without hand-styling its colours, the tooltip most of all. Bun downlevels light-dark() to a --buncss-light/--buncss-dark toggle but only defines the pair beside a color-scheme rule in the same stylesheet, which LayerChart doesn't ship — so define it once on :root in your shell:

/* shell.html — supply the definitions Bun's light-dark() downlevel expects */
:root {
  color-scheme: light dark;
  --buncss-light: initial;
  --buncss-dark: ;
}

/* System preference, unless the user explicitly picked light */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --buncss-light: ;
    --buncss-dark: initial;
  }
}

/* Explicit dark choice, regardless of system preference */
:root[data-theme='dark'] {
  --buncss-light: ;
  --buncss-dark: initial;
}

Interactive — SVG

The same library imported from layerchart/svg, hydrated via mochi:hydrate — hover the plot for the crosshair and tooltip. Leaving ssr off ships an empty frame; omitting width (which would override the measured container) is what makes it responsive. The tooltip sets portal: false because LayerChart otherwise portals it to <body>, escaping the themed frame and rendering unstyled.

<script lang="ts">
  import { AreaChart } from 'layerchart/svg';
  import { traffic, seriesColors } from './data.ts';

  const series = [
    { key: 'requests', label: 'Requests', color: seriesColors[0] },
    { key: 'cached', label: 'Cache hits', color: seriesColors[2] },
  ];
</script>

<!-- No ssr, no width: hydration measures the container. -->
<AreaChart data={traffic} x="month" {series} height={260} props={{ tooltip: { root: { portal: false } } }} />

Interactive — HTML

Imported from layerchart/html, so the bars are real DOM boxes instead of SVG. This one uses the <BarChart> shortcut — fine here because ssr is off, so the marks never render on the server. Hydration measures the container, so it reflows fluidly with no viewBox trick needed.

<script lang="ts">
  import { BarChart } from 'layerchart/html';
  import { traffic, seriesColors } from './data.ts';

  const series = [
    { key: 'requests', label: 'Requests', color: seriesColors[0] },
    { key: 'cached', label: 'Cache hits', color: seriesColors[2] },
  ];
</script>

<!-- HTML renderer: real DOM boxes; the BarChart shortcut is fine since ssr is off. -->
<BarChart data={traffic} x="month" {series} seriesLayout="group" legend height={260} />

Donut — SVG

A PieChart with a negative innerRadius and cRange colours; click a legend swatch to toggle a slice. Client-only is usually the better fit for a chart: since LayerChart draws nothing server-side anyway, hydrating would server-render an empty container and reconcile it, whereas a client-only island skips the server pass entirely and lets you ship real fallback markup — the skeleton below, wiped the moment the component mounts.

Loading chart…

Server-rendered image chart

You can server-render charts as an image (JPEG/PNG) using layerchart/server. This is possible today in Mochi but requires a bit of glue code code, see serverChart.ts in the tab view at the bottom of the demo for an example. The general recommendation if you want to go SSR-only is to use the SVG approach in the first example.

<script lang="ts">
  import { ServerChart } from 'layerchart/server';
  import { Axis, Grid, Spline } from 'layerchart';
  let { data, width, height, capture, onCapture } = $props();
</script>

<ServerChart {capture} {onCapture} {width} {height} {data} x="month" y="requests" yDomain={[0, null]}>
  <Grid y />
  <Axis placement="left" rule ticks={4} format={(v) => v / 1000 + 'k'} />
  <Axis placement="bottom" rule />
  <Spline stroke="rgb(59,130,246)" strokeWidth={2} />
</ServerChart>
// routes.ts — renders the chart to an image buffer on request
'/demos/charts/traffic.png': Mochi.api(async ({ url }) => {
  // See implementation of serverChart in the tabs below
  const { renderTrafficChart } = await import('./serverChart');
  const format = url.searchParams.get('format') === 'jpeg' ? 'jpeg' : 'png';
  const image = await renderTrafficChart({ width: 640, height: 240, format });
  return new Response(image, { headers: { 'Content-Type': 'image/' + format } });
}),
Server-rendered traffic chart, January through December
<script lang="ts">
  import CodeSnippet from '../../components/CodeSnippet.svelte';
  import StaticTrafficChart from './StaticTrafficChart.svelte';
  import TrafficChart from './TrafficChart.svelte';
  import HtmlBars from './HtmlBars.svelte';
  import RuntimeDonut from './RuntimeDonut.svelte';
  import Callout from '../../../../docs/_components/Callout.svelte';
  import { highlightCode } from '../../lib/highlight.server';
  import { files } from './files.ts';

  const install = await highlightCode('bun add layerchart', 'bash');

  const themingCss = await highlightCode(
    `/* shell.html — supply the definitions Bun's light-dark() downlevel expects */
:root {
  color-scheme: light dark;
  --buncss-light: initial;
  --buncss-dark: ;
}

/* System preference, unless the user explicitly picked light */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --buncss-light: ;
    --buncss-dark: initial;
  }
}

/* Explicit dark choice, regardless of system preference */
:root[data-theme='dark'] {
  --buncss-light: ;
  --buncss-dark: initial;
}`,
    'css',
  );

  const staticExample = await highlightCode(
    `<script lang="ts">
  import { Chart, Svg, Axis, Bars } from 'layerchart/svg';
  import { traffic } from './data.ts';
${'<'}/script>

<Chart ssr data={traffic} x="month" y="requests" c="requests" cRange={['var(--chart-1)']} width={600} height={220}>
  <Svg viewBox="0 0 600 220">
    <Axis placement="left" grid rule ticks={4} />
    <Axis placement="bottom" rule />
    <Bars fill="var(--chart-1)" />
  </Svg>
</Chart>`,
    'svelte',
  );

  const interactiveExample = await highlightCode(
    `<script lang="ts">
  import { AreaChart } from 'layerchart/svg';
  import { traffic, seriesColors } from './data.ts';

  const series = [
    { key: 'requests', label: 'Requests', color: seriesColors[0] },
    { key: 'cached', label: 'Cache hits', color: seriesColors[2] },
  ];
${'<'}/script>

<!-- No ssr, no width: hydration measures the container. -->
<AreaChart data={traffic} x="month" {series} height={260} props={{ tooltip: { root: { portal: false } } }} />`,
    'svelte',
  );

  const htmlExample = await highlightCode(
    `<script lang="ts">
  import { BarChart } from 'layerchart/html';
  import { traffic, seriesColors } from './data.ts';

  const series = [
    { key: 'requests', label: 'Requests', color: seriesColors[0] },
    { key: 'cached', label: 'Cache hits', color: seriesColors[2] },
  ];
${'<'}/script>

<!-- HTML renderer: real DOM boxes; the BarChart shortcut is fine since ssr is off. -->
<BarChart data={traffic} x="month" {series} seriesLayout="group" legend height={260} />`,
    'svelte',
  );

  const serverComponentExample = await highlightCode(
    `<script lang="ts">
  import { ServerChart } from 'layerchart/server';
  import { Axis, Grid, Spline } from 'layerchart';
  let { data, width, height, capture, onCapture } = $props();
${'<'}/script>

<ServerChart {capture} {onCapture} {width} {height} {data} x="month" y="requests" yDomain={[0, null]}>
  <Grid y />
  <Axis placement="left" rule ticks={4} format={(v) => v / 1000 + 'k'} />
  <Axis placement="bottom" rule />
  <Spline stroke="rgb(59,130,246)" strokeWidth={2} />
</ServerChart>`,
    'svelte',
  );

  const serverRouteExample = await highlightCode(
    `// routes.ts — renders the chart to an image buffer on request
'/demos/charts/traffic.png': Mochi.api(async ({ url }) => {
  // See implementation of serverChart in the tabs below
  const { renderTrafficChart } = await import('./serverChart');
  const format = url.searchParams.get('format') === 'jpeg' ? 'jpeg' : 'png';
  const image = await renderTrafficChart({ width: 640, height: 240, format });
  return new Response(image, { headers: { 'Content-Type': 'image/' + format } });
}),`,
    'typescript',
  );

  const sources = await loadSources(files);
</script>

<p>
  Minimal setup: install it, import from <code>layerchart</code>, and Mochi bundles it into whichever island imports it. Full component reference at
  <a href="https://www.layerchart.com" target="_blank" rel="noopener noreferrer">layerchart.com</a>.
</p>
<CodeSnippet html={install} />

<h2>Pure SSR — SVG</h2>
<p>
  LayerChart can render to pure SVG or HTML by omitting Mochi's hydrate directives. That needs two opt-ins: <code>ssr</code> (LayerChart skips server rendering otherwise) and
  explicit
  <code>width</code>/<code>height</code> to set the dimensions, since nothing resizes the container client side.
</p>
<p>
  SVG is recommended for pure SSR graphs: the <code>&lt;Svg&gt;</code> gets a matching <code>viewBox</code>, so the fixed 600&times;220 geometry rescales to any column width with
  CSS alone.
</p>
<p>
  It's composed from <code>&lt;Chart&gt;</code> primitives rather than the <code>&lt;BarChart&gt;</code> shortcut: that shortcut's <code>marks</code> snippet shadows its own
  <code>marks</code> prop, which the Svelte server compiler turns into unbounded recursion once <code>ssr</code> forces a server render.
</p>
<Callout type="warning">
  <strong>LayerChart's SSR pitfall.</strong> LayerChart doesn't officially support SSR for its SVG and HTML renderers, and its all-in-one chart components (<code
    >&lt;BarChart&gt;</code
  >,
  <code>&lt;AreaChart&gt;</code>, <code>&lt;LineChart&gt;</code>, <code>&lt;PieChart&gt;</code>, <code>&lt;ScatterChart&gt;</code>, <code>&lt;ArcChart&gt;</code>) can't be
  server-rendered — turning on <code>ssr</code> crashes the whole page. For a no-JavaScript chart, build it from the smaller pieces like this one does.
</Callout>
<CodeSnippet html={staticExample} />
<StaticTrafficChart />

<h2>Theming — light/dark</h2>
<p>
  LayerChart ships its tooltip, axis, and other default styles pre-coloured with CSS <code>light-dark()</code> so they read correctly in both modes out of the box — you lean on
  that any time you use a LayerChart component without hand-styling its colours, the tooltip most of all. Bun downlevels <code>light-dark()</code> to a
  <code>--buncss-light</code>/<code>--buncss-dark</code> toggle but only defines the pair beside a <code>color-scheme</code> rule in the same stylesheet, which LayerChart doesn't
  ship — so define it once on <code>:root</code> in your shell:
</p>
<CodeSnippet html={themingCss} />

<h2>Interactive — SVG</h2>
<p>
  The same library imported from <code>layerchart/svg</code>, hydrated via <code>mochi:hydrate</code> — hover the plot for the crosshair and tooltip. Leaving <code>ssr</code> off
  ships an empty frame; omitting
  <code>width</code> (which would override the measured container) is what makes it responsive. The tooltip sets <code>portal: false</code> because LayerChart otherwise portals
  it to <code>&lt;body&gt;</code>, escaping the themed frame and rendering unstyled.
</p>
<Callout type="info">
  <strong>Helpers are fine when hydrating.</strong> With <code>ssr</code> off, the marks never render on the server, so the all-in-one components like
  <code>&lt;AreaChart&gt;</code>
  work here — no need to compose from primitives the way the pure-SSR chart does.
</Callout>
<CodeSnippet html={interactiveExample} />
<TrafficChart mochi:hydrate />

<h2>Interactive — HTML</h2>
<p>
  Imported from <code>layerchart/html</code>, so the bars are real DOM boxes instead of SVG. This one uses the <code>&lt;BarChart&gt;</code> shortcut — fine here because
  <code>ssr</code> is off, so the marks never render on the server. Hydration measures the container, so it reflows fluidly with no <code>viewBox</code> trick needed.
</p>
<CodeSnippet html={htmlExample} />
<HtmlBars mochi:hydrate />

<h2>Donut — SVG</h2>
<p>
  A <code>PieChart</code> with a negative <code>innerRadius</code> and <code>cRange</code> colours; click a legend swatch to toggle a slice. Client-only is usually the better fit for
  a chart: since LayerChart draws nothing server-side anyway, hydrating would server-render an empty container and reconcile it, whereas a client-only island skips the server pass
  entirely and lets you ship real fallback markup — the skeleton below, wiped the moment the component mounts.
</p>
<RuntimeDonut mochi:clientOnly>
  <div class="skeleton">Loading chart…</div>
</RuntimeDonut>

<h2>Server-rendered image chart</h2>
<p>
  You can server-render charts as an image (JPEG/PNG) using <code>layerchart/server</code>. This is possible today in Mochi but requires a bit of glue code code, see
  <code>serverChart.ts</code> in the tab view at the bottom of the demo for an example. The general recommendation if you want to go SSR-only is to use the SVG approach in the first
  example.
</p>
<CodeSnippet html={serverComponentExample} />
<CodeSnippet html={serverRouteExample} />
<img class="server-png" src="/demos/charts/traffic.png" alt="Server-rendered traffic chart, January through December" width="640" height="240" />
Styles
<style>
  p {
    margin: 0 0 0.75rem;
    font-size: 0.95rem;
    color: var(--text-muted);
    line-height: 1.55;
  }

  p a {
    color: var(--accent);
    font-weight: 600;
  }

  .skeleton {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 324px;
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-md);
    background: var(--surface-muted);
    color: var(--text-subtle);
    font-size: 0.9rem;
  }

  .server-png {
    display: block;
    width: 100%;
    max-width: 640px;
    height: auto;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    background: #fff;
  }

  code {
    background: var(--code-bg);
    color: var(--code-accent);
    padding: 0.05rem 0.35rem;
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: 0.85rem;
  }
</style>