ApexCharts with Next.js
ApexCharts requires a live browser DOM to mount its SVG canvas, measure container dimensions, and attach event listeners. Next.js App Router runs most of your code on the server by default, which means you need to be deliberate about where ApexCharts executes. This page covers the three main integration patterns and explains when to reach for each one.
Installation
npm install react-apexcharts apexcharts
react-apexcharts is the official React wrapper. It already has "use client" at the top of its own source, so the library itself will not run during server rendering. apexcharts is the core library and is listed as a peer dependency (>=5.10.1 in react-apexcharts 2.1.1, which includes 8.x).
Pattern A: Client Component (most common)
The standard approach is to wrap the chart in a Client Component. Any component file that contains 'use client' at the top will be rendered in the browser, which is where ApexCharts needs to run.
// components/SalesChart.tsx
'use client'
import ReactApexChart from 'react-apexcharts'
import type { ApexOptions } from 'apexcharts'
const options: ApexOptions = {
chart: { type: 'bar' },
xaxis: { categories: ['Jan', 'Feb', 'Mar'] }
}
const series = [{ name: 'Sales', data: [44, 55, 41] }]
export default function SalesChart() {
return (
<ReactApexChart
type="bar"
series={series}
options={options}
height={350}
/>
)
}
You can then import this component from a Server Component page without adding 'use client' to the page itself. Next.js automatically treats SalesChart as a client boundary.
// app/dashboard/page.tsx (Server Component, no 'use client' needed)
import SalesChart from '@/components/SalesChart'
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
<SalesChart />
</main>
)
}
The 'use client' directive on SalesChart.tsx marks a boundary. Everything inside that file and its imports runs in the browser. Everything in page.tsx above it can still run on the server, including async/await data fetches, database calls, and so on.
Chart types outside the default bundle
react-apexcharts draws with the default apexcharts bundle. Since 8.0 that bundle leaves out three chart types (unit, with waffle and beeswarm; sunburst; violin) and four features (drilldown, waterfall, dumbbell, streamgraph). Import the one you use in the Client Component file, next to the wrapper:
// components/TrafficSunburst.tsx
'use client'
import ReactApexChart from 'react-apexcharts'
import 'apexcharts/sunburst'
const series = [{
data: [
{ x: 'Mobile', y: 55, children: [{ x: 'iOS', y: 30 }, { x: 'Android', y: 25 }] },
{ x: 'Desktop', y: 33, children: [{ x: 'Windows', y: 20 }, { x: 'macOS', y: 13 }] }
]
}]
export default function TrafficSunburst() {
return (
<ReactApexChart
// @ts-expect-error react-apexcharts 2.1.1 types list chart types only up to 'treemap'
type="sunburst"
series={series}
options={{}}
height={400}
/>
)
}
A feature is apexcharts/features/<name>, for example apexcharts/features/waterfall (not apexcharts/waterfall, which is for the lean core). Without the import, the browser console names the line to add, and a chart whose type is missing draws nothing. The React guide has the full list.
Pattern B: Dynamic import with ssr: false
When you want to prevent the chart bundle from being included in the server-rendered HTML at all, use Next.js's dynamic() helper with ssr: false. This is useful for:
- Reducing server payload for charts that are below the fold
- Lazy-loading chart code only when the user navigates to a page
- Integrating a third-party chart component you cannot modify to add
'use client'
// app/dashboard/page.tsx
import dynamic from 'next/dynamic'
const SalesChart = dynamic(() => import('@/components/SalesChart'), {
ssr: false,
loading: () => <div style={{ height: 350 }}>Loading chart...</div>
})
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
<SalesChart />
</main>
)
}
The loading prop renders a placeholder while the chart JavaScript downloads and hydrates. Setting the placeholder height to match the chart height avoids layout shift.
Note that ssr: false only prevents the chart from rendering during the server pass. The underlying SalesChart component still needs 'use client' if it uses React state, because it will run in the browser once the bundle loads.
Pattern C: Server-rendered SVG
For static charts where interactivity is not needed (PDF reports, email previews, read-only dashboards), ApexCharts can render the chart on the server. No browser is involved. renderToHTML() below returns the SVG with the legend beside it as HTML, plus the legend's stylesheet, when the chart shows one. For a plain SVG string, for a PDF renderer or an email, use renderToString(), which returns the SVG alone, without the legend.
// app/report/page.tsx (Server Component)
import ApexCharts from 'apexcharts/ssr'
export default async function ReportPage() {
const html = await ApexCharts.renderToHTML(
{
chart: { type: 'bar' },
series: [{ name: 'Sales', data: [44, 55, 41] }],
xaxis: { categories: ['Jan', 'Feb', 'Mar'] }
},
{ width: 600, height: 350 }
)
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
The dangerouslySetInnerHTML call is safe in this context. The HTML comes from renderToHTML, which builds the SVG, and the legend beside it when the chart shows one, from your own chart configuration. When series names or labels come from data you do not control, use ApexCharts 8.0.1 or later, which escapes the text and attributes it writes into the SVG, and remove markup from the names the legend lists (series names, a pie's labels, a distributed bar's categories), which it writes as HTML (see Rendering Data You Do Not Control). The chart is fully rendered before the page reaches the browser, so it appears immediately without any JavaScript hydration.
A chart type or feature outside the default bundle takes the same import here, next to apexcharts/ssr:
// app/report/page.tsx (Server Component)
import ApexCharts from 'apexcharts/ssr'
import 'apexcharts/sunburst'
Without it, renderToHTML() rejects with SSR rendering failed: and the line that names the import, so the Server Component throws instead of returning an empty chart. The same goes for react-apexcharts/server, which renders through apexcharts/ssr: put the import in the Server Component file that uses it.
For the full SSR API reference, see Server-Side Rendering.
Fetching data server-side, rendering client-side
The recommended pattern for dashboards is to fetch data in a Server Component and pass it as props to a Client Component chart. This keeps your API calls on the server (so API keys stay private and there is no client-side waterfall) while the chart itself stays interactive.
// app/dashboard/page.tsx (Server Component)
import SalesChart from '@/components/SalesChart'
async function getSalesData(): Promise<number[]> {
const res = await fetch('https://api.example.com/sales', {
next: { revalidate: 3600 }
})
return res.json()
}
export default async function DashboardPage() {
const data = await getSalesData()
return (
<main>
<h1>Dashboard</h1>
<SalesChart data={data} />
</main>
)
}
// components/SalesChart.tsx
'use client'
import ReactApexChart from 'react-apexcharts'
import type { ApexOptions } from 'apexcharts'
interface Props {
data: number[]
}
const options: ApexOptions = {
chart: { type: 'bar' },
xaxis: { categories: ['Jan', 'Feb', 'Mar'] }
}
export default function SalesChart({ data }: Props) {
return (
<ReactApexChart
type="bar"
series={[{ name: 'Sales', data }]}
options={options}
height={350}
/>
)
}
The rule is: props crossing a Server-to-Client boundary must be serializable (plain objects, arrays, strings, numbers). Functions and class instances cannot cross this boundary.
Updating chart data based on user interaction
When the chart needs to respond to user actions (date range pickers, filters, tab switches), manage the series state inside the Client Component. You can still seed the initial data from the server via props.
// components/FilterableChart.tsx
'use client'
import { useState } from 'react'
import ReactApexChart from 'react-apexcharts'
import type { ApexOptions } from 'apexcharts'
interface Props {
initialData: number[]
}
const options: ApexOptions = {
chart: { type: 'bar' },
xaxis: { categories: ['Jan', 'Feb', 'Mar'] }
}
export default function FilterableChart({ initialData }: Props) {
const [series, setSeries] = useState([{ name: 'Sales', data: initialData }])
async function fetchFiltered(period: string) {
const data: number[] = await fetch(`/api/sales?period=${period}`).then(r => r.json())
setSeries([{ name: 'Sales', data }])
}
return (
<>
<div style={{ marginBottom: 12 }}>
<button onClick={() => fetchFiltered('week')}>Last week</button>
<button onClick={() => fetchFiltered('month')}>Last month</button>
</div>
<ReactApexChart
type="bar"
series={series}
options={options}
height={350}
/>
</>
)
}
When setSeries is called, React re-renders the component and react-apexcharts diffs the new series against the previous one, animating the transition automatically.
Common mistakes
Assuming apexcharts cannot be imported in a Server Component. It can, and it does not throw. The package's exports map carries a node condition and no react-server key, so an import from a Server Component resolves to dist/apexcharts.ssr.esm.js and evaluates in Node without touching a DOM global (checked on apexcharts 8.0.0). What you get is the SSR build, which renders to a string rather than mounting, so a chart you expected to be interactive simply will not be. Reach for react-apexcharts when you want client rendering, and renderToHTML when you want server-side SVG output on purpose.
A sunburst, unit or violin chart that draws nothing after upgrading to 8.0. Those three chart types, and the drilldown, waterfall, dumbbell and streamgraph features, are no longer in the default bundle. The browser console names the import to add. Put it in the Client Component file (see Pattern A), and next to apexcharts/ssr for a server-rendered chart.
Forgetting 'use client' when the chart component uses state. If your chart component calls useState, useEffect, or any other React hook, it must be a Client Component. Omitting 'use client' causes Next.js to throw a build-time error explaining that hooks cannot run in Server Components.
Accessing window at module scope inside a dynamically imported component. Using dynamic(() => import(...), { ssr: false }) prevents the component from rendering on the server, but the module-level code in that file is still evaluated on the server before the component function runs. Any window reference at the top level of the module (outside of useEffect or event handlers) will still throw. Move those references inside a useEffect callback or a user event handler.
Related
- React Charts - full guide to the
react-apexchartswrapper, props, and TypeScript types - Server-Side Rendering - complete
apexcharts/ssrAPI reference