D3 charts in SvelteKit
D3 pairs well with Svelte when you keep it scoped to a single SVG and drive updates through resize events or reactive data. Treat D3 as a rendering layer, and let Svelte own the lifecycle.
Best practices
- Initialize D3 only in
onMountto avoid SSR mismatch errors. - Use
ResizeObserveron the chart container to re-render on layout changes. - Read theme tokens from CSS variables so charts stay in sync with the shell.
- Keep D3 updates idempotent: clear the SVG before re-rendering.
- Pad your y-domain so the line never sits on the edges.
- Cleanup observers and listeners on destroy to avoid leaks.
SvelteKit-friendly setup
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import * as d3 from "d3";
let svgEl;
let wrapperEl;
let resizeObserver;
const data = Array.from({ length: 24 }, (_, index) => ({
index,
value: 0.6 + 0.2 * Math.sin(index / 2)
}));
const renderChart = () => {
const width = Math.max(320, wrapperEl.getBoundingClientRect().width);
const height = 260;
const margin = { top: 24, right: 24, bottom: 36, left: 48 };
const svg = d3.select(svgEl);
svg.selectAll("*").remove();
svg.attr("width", width).attr("height", height);
const x = d3.scaleLinear().domain([0, data.length - 1]).range([margin.left, width - margin.right]);
const y = d3.scaleLinear().domain([0, 1]).range([height - margin.bottom, margin.top]);
const line = d3.line().x((d) => x(d.index)).y((d) => y(d.value));
svg.append("path").datum(data).attr("fill", "none").attr("stroke", "currentColor").attr("d", line);
};
onMount(() => {
renderChart();
resizeObserver = new ResizeObserver(renderChart);
resizeObserver.observe(wrapperEl);
});
onDestroy(() => {
resizeObserver?.disconnect();
});
</script> Checklist
- Container has a fixed height so SVG layout is stable.
- Axes use
tickSizeOuter(0)for clean endpoints. - Gridlines use low-contrast strokes so the data pops.