ApexCharts with SvelteKit
Using ApexCharts in SvelteKit
ApexCharts has no official Svelte wrapper, which means you use the vanilla apexcharts package directly. This is not a limitation. The vanilla API maps cleanly onto Svelte's lifecycle hooks: onMount gives you a browser-only execution context, bind:this gives you a reference to the DOM node, and onDestroy lets you clean up before the component leaves the page. The result is a handful of lines with no framework-specific abstraction layer to learn or maintain.
Installation
Install the apexcharts package:
npm install apexcharts
No additional Svelte adapter or plugin is required.
Basic chart component (Svelte 5)
The example below uses Svelte 5 runes syntax. The comments explain the role each piece plays.
<!-- BarChart.svelte -->
<script>
import { onMount, onDestroy } from 'svelte'
import ApexCharts from 'apexcharts'
// $state makes chartEl reactive so bind:this can write to it
let chartEl = $state(null)
let chart
const options = {
chart: { type: 'bar', height: 350 },
series: [{ name: 'Sales', data: [44, 55, 41, 67, 22] }],
xaxis: { categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May'] }
}
// onMount only runs in the browser, never during SSR.
// This is the correct place to create an ApexCharts instance
// because ApexCharts reads window and document on construction.
onMount(() => {
chart = new ApexCharts(chartEl, options)
chart.render()
})
// onDestroy runs when the component is removed from the DOM.
// Calling destroy() removes the chart's event listeners and DOM nodes,
// preventing memory leaks on page navigation.
onDestroy(() => {
chart?.destroy()
})
</script>
<!-- bind:this writes the real DOM element into chartEl after mount -->
<div bind:this={chartEl}></div>
bind:this is the bridge between Svelte's component tree and the imperative ApexCharts API. ApexCharts needs a real DOM element to mount into, and bind:this provides it at the moment the element is inserted into the document. Because onMount fires after that insertion, chartEl is always populated by the time you call new ApexCharts(chartEl, options).
Svelte 4 equivalent
If your project is on Svelte 4, the pattern is the same. Replace $state with a plain let declaration. The $state rune does not exist in Svelte 4, but bind:this works identically.
<!-- BarChart.svelte (Svelte 4) -->
<script>
import { onMount, onDestroy } from 'svelte'
import ApexCharts from 'apexcharts'
let chartEl
let chart
const options = {
chart: { type: 'bar', height: 350 },
series: [{ name: 'Sales', data: [44, 55, 41, 67, 22] }],
xaxis: { categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May'] }
}
onMount(() => {
chart = new ApexCharts(chartEl, options)
chart.render()
})
onDestroy(() => {
chart?.destroy()
})
</script>
<div bind:this={chartEl}></div>
Reactive data updates
When series data changes after the chart is rendered, call chart.updateSeries() rather than destroying and recreating the chart. ApexCharts animates the transition between the old and new data automatically.
Svelte 5: using $effect
$effect runs after every render where its tracked dependencies change. The if (chart) guard prevents it from firing before onMount has created the chart instance.
<!-- RevenueChart.svelte (Svelte 5) -->
<script>
import { onMount, onDestroy } from 'svelte'
import ApexCharts from 'apexcharts'
// data is passed in as a prop from the parent component
let { data = [] } = $props()
let chartEl = $state(null)
let chart
onMount(() => {
chart = new ApexCharts(chartEl, {
chart: { type: 'bar', height: 350 },
series: [{ name: 'Revenue', data }],
xaxis: { categories: ['Q1', 'Q2', 'Q3', 'Q4'] }
})
chart.render()
})
// $effect re-runs whenever `data` changes.
// Because chart is declared outside the effect,
// the guard ensures the update only runs after render.
$effect(() => {
if (chart) {
chart.updateSeries([{ name: 'Revenue', data }])
}
})
onDestroy(() => chart?.destroy())
</script>
<div bind:this={chartEl}></div>
Svelte 4: using $: reactive statements
In Svelte 4, reactive statements ($:) serve the same purpose as $effect. The statement re-runs whenever any variable it reads changes.
<!-- RevenueChart.svelte (Svelte 4) -->
<script>
import { onMount, onDestroy } from 'svelte'
import ApexCharts from 'apexcharts'
export let data = []
let chartEl
let chart
onMount(() => {
chart = new ApexCharts(chartEl, {
chart: { type: 'bar', height: 350 },
series: [{ name: 'Revenue', data }],
xaxis: { categories: ['Q1', 'Q2', 'Q3', 'Q4'] }
})
chart.render()
})
// $: re-evaluates when `data` changes.
// The `chart` check prevents running before onMount completes.
$: if (chart) {
chart.updateSeries([{ name: 'Revenue', data }])
}
onDestroy(() => chart?.destroy())
</script>
<div bind:this={chartEl}></div>
Fetching data with +page.server.js
SvelteKit's load function runs on the server and passes its return value as the data prop on the page component. You can use this to fetch chart data server-side and pass it into the chart on mount.
// src/routes/sales/+page.server.js
export async function load({ fetch }) {
const res = await fetch('/api/sales')
const salesData = await res.json()
return { salesData }
}
<!-- src/routes/sales/+page.svelte (Svelte 5) -->
<script>
import { onMount, onDestroy } from 'svelte'
import ApexCharts from 'apexcharts'
// SvelteKit injects the load() return value here automatically
let { data } = $props()
let chartEl = $state(null)
let chart
onMount(() => {
chart = new ApexCharts(chartEl, {
chart: { type: 'bar', height: 350 },
series: [{ name: 'Sales', data: data.salesData }],
xaxis: { categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May'] }
})
chart.render()
})
onDestroy(() => chart?.destroy())
</script>
<div bind:this={chartEl}></div>
The load function fetches data before the page is sent to the browser, so data.salesData is available the moment onMount fires. There is no need for a loading spinner in this pattern.
Dynamic import for code splitting
By default, import ApexCharts from 'apexcharts' adds ApexCharts to your main JavaScript bundle, which every visitor downloads even on pages that have no charts. A dynamic import moves ApexCharts into a separate chunk that loads on demand, only when the component mounts.
<!-- BarChart.svelte -->
<script>
import { onMount, onDestroy } from 'svelte'
let chartEl
let chart
onMount(async () => {
// ApexCharts is fetched as a separate network request,
// so the main bundle stays small.
const { default: ApexCharts } = await import('apexcharts')
chart = new ApexCharts(chartEl, {
chart: { type: 'bar', height: 350 },
series: [{ name: 'Sales', data: [44, 55, 41, 67, 22] }],
xaxis: { categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May'] }
})
chart.render()
})
onDestroy(() => chart?.destroy())
</script>
<div bind:this={chartEl}></div>
The trade-off is that the chart appears slightly later because the browser must fetch the chunk before render() runs. On fast connections the difference is imperceptible. On slow connections it can be meaningful, so benchmark before committing to this approach.
Multiple charts in one component
Each chart instance needs its own DOM element reference and its own variable to hold the instance. The cleanup in onDestroy must cover every instance.
<script>
import { onMount, onDestroy } from 'svelte'
import ApexCharts from 'apexcharts'
let lineEl, barEl
let lineChart, barChart
const lineOptions = {
chart: { type: 'line', height: 300 },
series: [{ name: 'Visitors', data: [30, 40, 35, 50, 49, 60] }],
xaxis: { categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] }
}
const barOptions = {
chart: { type: 'bar', height: 300 },
series: [{ name: 'Revenue', data: [44, 55, 41, 67, 22, 43] }],
xaxis: { categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] }
}
onMount(() => {
lineChart = new ApexCharts(lineEl, lineOptions)
barChart = new ApexCharts(barEl, barOptions)
lineChart.render()
barChart.render()
})
onDestroy(() => {
lineChart?.destroy()
barChart?.destroy()
})
</script>
<div bind:this={lineEl}></div>
<div bind:this={barEl}></div>
SSR behavior
SvelteKit prerenders pages and renders them on the server by default. ApexCharts is browser-only; it accesses window, document, and SVG APIs that do not exist in a Node.js environment. The onMount callback only runs in the browser, which means the guard is built in. The chart container renders as an empty <div> in the server HTML and the chart fills it after hydration.
No special configuration is required. The pattern shown in all examples above is SSR-safe without any extra steps.
Disabling SSR for a route (optional)
If a page is chart-heavy and you want to skip server rendering entirely, you can opt out at the route level:
// src/routes/dashboard/+page.js
export const ssr = false
With ssr = false, SvelteKit skips server rendering and sends an empty HTML shell. The entire page, including the chart containers, is built in the browser. This reduces server load for pages where server-rendered HTML provides little benefit, such as dashboard pages behind authentication. The trade-off is that users see a blank page until JavaScript executes rather than seeing server-rendered content immediately.