Skip to content

Compiled component wrapper ​

@chart-on/svelte wraps the engine and the shell as components, an action and context helpers for apps built with a compiler-based component framework (version 5.18 or later, with runes), including apps whose app framework renders every page on the server first. It follows the wrapper contract: the chart is created when the component mounts and destroyed when it unmounts, and changing a prop updates the chart in place instead of creating a new one.

ExportFromWhat it is
<Chart>@chart-on/svelteThe engine alone, in a div.
use:chart@chart-on/svelteThe engine in an element of your own, for minimal set-ups.
useChart()@chart-on/svelteThe Chart of the nearest <Chart> or <ChartShell>, as .current.
<ChartShell>@chart-on/svelte/shellThe widget: toolbar, drawing rail, bottom bar and the chart.
useChartShell()@chart-on/svelte/shellThe ChartShell of the nearest <ChartShell>, as .current.

Install ​

bash
pnpm add @chart-on/svelte @chart-on/core
# Only for <ChartShell>:
pnpm add @chart-on/ui

The framework (version 5.18 or later), @chart-on/core and @chart-on/ui are peer dependencies: the app installs them once and the wrapper uses the app's copies. @chart-on/ui is optional, because <ChartShell> lives in its own entry, @chart-on/svelte/shell. An app that only imports @chart-on/svelte never loads it.

The package ships its components as source, with the types already taken out, as the framework's packages usually do: the app's own compiler builds them, for the browser and for the server, with the same version and settings as the rest of the app. The framework's bundler plugin picks them up through the svelte export condition; nothing needs configuring.

The engine ​

Give the component a height; the chart fills its container. class, style and other attributes go to the container div.

svelte
<script lang="ts">
  import { createMockDataSource, dataSource as dataSourcePlugin } from '@chart-on/core';
  import { Chart } from '@chart-on/svelte';

  let { symbol }: { symbol: string } = $props();
  // Created once per component instance: a new source reloads the data.
  const dataSource = createMockDataSource();
  const plugins = [dataSourcePlugin()];
</script>

<Chart style="height: 480px" options={{ barSpacing: 8, plugins }} {dataSource} {symbol} interval="1h" />
PropApplied withNotes
optionscreateChart at mount, then chart.applyOptionsPlain data in it is compared by value, so an object literal in the markup does not apply again when it is rebuilt, and a change inside a $state object applies. Functions, dates and class instances inside it are compared by identity: a formatter written inline in the markup is new each time and applies the options again, so define callbacks once in the script. The chart gets a plain copy, never a state proxy. Leaving out a key does not reset it; pass the value you want.
dataSourcechart.setDataSourceChanging it attaches the new source and loads symbol again. Keep it in a const or in $state.raw. Deep $state wraps a source made of plain objects in a proxy, and the wrapper cannot tell a proxy from the object, so the chart gets the proxy and calls through it: it still works, but every call the chart makes on it goes through the state system. Removing the prop keeps the current source, and a new symbol or interval still loads from it; pass null to detach it. The chart needs the dataSource() plugin in options.plugins (or builtins()); without it the source is not attached and development builds warn once (data-source-missing).
symbol, intervalchart.setSymbolLoads once both are set and a dataSource is given; loads again when either changes. A failed load emits data:error.
onchart.onEvent callbacks, see Events.

A prop left out, or set to undefined after having a value, leaves the chart as it is: the wrapper stops controlling it but does not reset it, and keeps using its last value when another prop changes. Setting it back to that same value applies nothing. To detach the data source, pass null.

A few options are fixed when the chart is created, for example gl. Changing them in options later has no effect; remount the component on purpose instead, for example inside a {#key} block.

Reaching the chart ​

The component gives the app the Chart it created, with the whole imperative API. It exists only while the component is mounted: it is null during server rendering and before the first mount. The chart is a class instance, so $state keeps it as it is, without a proxy; $state.raw makes that explicit.

With bind:chart:

svelte
<script lang="ts">
  import type { Chart as ChartInstance } from '@chart-on/core';
  import { Chart } from '@chart-on/svelte';

  let chart: ChartInstance | null = $state.raw(null);
</script>

<button onclick={() => chart?.fitContent()}>Fit</button>
<Chart bind:chart style="height: 400px" />

The component sets the binding; the app only reads it.

With useChart() in a component rendered inside <Chart>. Its current is null until the chart exists, so an $effect that reads it runs again at the right time, and the effect's clean-up runs before the chart is destroyed:

svelte
<!-- Candles.svelte -->
<script lang="ts">
  import type { Bar } from '@chart-on/core';
  import { useChart } from '@chart-on/svelte';

  let { bars }: { bars: Bar[] } = $props();
  const chart = useChart();

  $effect(() => {
    const current = chart.current;
    if (!current) return;
    const series = current.addCandles();
    series.setData(bars);
    return () => current.removeSeries(series);
  });
</script>
svelte
<Chart style="height: 400px">
  <Candles {bars} />
</Chart>

Call useChart() while the component is being set up, as with getContext. Outside a chart component its current is always null.

For live ticks, keep the series handle and call series.update(bar): data never travels through a remount.

Events ​

The on prop takes one callback per event, keyed by the chart's own event names. Only the events listed are subscribed, so leave out the busy ones (crosshair:move, frame:render) unless you use them. The payload is passed on unchanged.

svelte
<Chart
  style="height: 400px"
  on={{
    'crosshair:move': (e) => (hover = e?.time ?? null),
    'data:error': (e) => console.warn(e.symbol, e.error),
  }}
/>

A new callback for the same event is used straight away without subscribing again, so inline functions are fine. Removing a key unsubscribes it, and unmounting unsubscribes everything. The events the first load emits while the component mounts, such as symbol:change, already reach the callbacks.

Events come through one prop rather than one on… prop per event because the chart and the shell share some event names (theme:change): on and shellOn say which one you mean.

The action ​

For a page that needs only the engine and no children, use:chart mounts it in an element you already have. It takes the same options, dataSource, symbol, interval and on as <Chart>, compared the same way, plus ref, a callback that receives the chart after it is created and null after it is destroyed. When the parameters bring a different ref, the old one receives null and the new one the chart, so an inline arrow function is called again on every update; define it once in the script to avoid that.

svelte
<script lang="ts">
  import { createMockDataSource, type Chart as ChartInstance } from '@chart-on/core';
  import { chart } from '@chart-on/svelte';

  let { symbol }: { symbol: string } = $props();
  const dataSource = createMockDataSource();
  let instance: ChartInstance | null = $state.raw(null);
  const keep = (created: ChartInstance | null) => (instance = created);
</script>

<div style="height: 400px" use:chart={{ dataSource, symbol, interval: '1h', ref: keep }}></div>

Actions run only in the browser, so the action is as safe in server-rendered pages as the components. There is no useChart() inside the element: use ref.

The widget ​

<ChartShell> mounts the full shell. Import its stylesheet once in the app.

svelte
<script lang="ts">
  import { ChartShell } from '@chart-on/svelte/shell';
  import '@chart-on/ui/style.css';

  let { dark }: { dark: boolean } = $props();
</script>

<ChartShell
  style="height: 600px"
  theme={dark ? 'dark' : 'light'}
  features={{ tooltip: true }}
  shellOptions={{ navigator: true }}
  shellOn={{ 'theme:change': (e) => console.log(e.resolved) }}
/>

It takes the same options, dataSource, symbol, interval and on props and container attributes as <Chart>, plus:

PropApplied withNotes
themeshell.setThemedark, light or system.
paletteshell.setPalette
featuresshell.applyFeaturesCompared by value.
shellOptionscreateChartShell at mountToolbar, rail, dialogs and the rest of the set-up. Read once; remount, for example inside a {#key} block, to change them.
shellOnshell.onShell event callbacks, like on.

As with <Chart>, setting theme, palette or features to undefined leaves the shell as it is, including a theme the user picked in the shell. Setting the prop again applies it again.

bind:shell reaches the shell and bind:chart its chart. Inside the shell, useChart() holds that chart and useChartShell() the shell.

Server rendering ​

Both entries import on a server without touching a browser global, and both components render on a server as their container div with their children inside. The chart is created in onMount, which runs only in the browser, after the page has hydrated. Nothing is needed to opt in: an app framework that renders every page on the server and then hydrates it can use the components like any other, and useChart() holds null until the browser mounts the chart.

To skip server rendering for a page altogether, for example a page that only makes sense with a chart, turn it off for that route in the app framework. The wrapper does not need it.

Example apps ​

Two small apps in the repository show the components in use:

  • examples/svelte-bundler: the widget in a client-rendered app, with a theme switch, fitContent() through bind:shell and a crosshair readout through on.
  • examples/svelte-ssr: <Chart> rendered to HTML on the server and hydrated in the browser, served by a small Node server.

CI builds both and renders the server example in Node with node scripts/check-examples.mjs.

Tests ​

Tests mount each component and the action, change every prop and check that the same instance received the matching method call, check that events the first load emits while mounting reach the callbacks, check that children's effects clean up before the chart is destroyed, unmount and check that no WebGL context or animation frame is left (also over 100 mount and unmount cycles), render both components on a server, and import both entries on a server without touching a browser global.