Dark Mode Charts
ApexCharts lets you switch a chart to dark mode by setting theme.mode: 'dark' in the options object. This adjusts the colors of axis labels, grid lines, tooltips, and crosshairs. It does not touch the chart container background — that is a CSS concern, and keeping them separate gives you full control over each independently.
Basic dark mode chart
Pass theme.mode: 'dark' when constructing the chart. Then set the container background in CSS to match the dark palette you want. ApexCharts renders the chart SVG with transparent fill by default, so whatever the container shows through becomes the visual background.
<div id="chart"></div>
#chart {
background: #1a1a2e;
border-radius: 8px;
padding: 16px;
}
import ApexCharts from 'apexcharts'
const options = {
chart: {
type: 'line',
height: 350
},
theme: {
mode: 'dark'
},
series: [
{ name: 'Revenue', data: [30, 40, 35, 50, 49, 60, 70] }
],
xaxis: {
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul']
}
}
const chart = new ApexCharts(document.querySelector('#chart'), options)
chart.render()
Why the background is separate: theme.mode controls the text and line colors that ApexCharts draws inside the SVG. The SVG root has no background fill — it is transparent. If you set theme.mode: 'dark' without adding a dark background to the container, you get dark-colored labels floating over a white page, which is worse than either mode by itself. Always set the container background alongside the theme.
Manual toggle
Use updateOptions to switch the theme at runtime. There is no need to destroy and recreate the chart. ApexCharts merges the new partial config and re-renders only what changed.
<button id="theme-toggle">Toggle dark mode</button>
<div id="chart"></div>
import ApexCharts from 'apexcharts'
const options = {
chart: { type: 'bar', height: 350 },
theme: { mode: 'light' },
series: [{ name: 'Sales', data: [44, 55, 41, 67, 22, 43] }],
xaxis: { categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] }
}
const el = document.querySelector('#chart')
const chart = new ApexCharts(el, options)
chart.render()
let isDark = false
document.querySelector('#theme-toggle').addEventListener('click', () => {
isDark = !isDark
chart.updateOptions({ theme: { mode: isDark ? 'dark' : 'light' } })
el.style.background = isDark ? '#1a1a2e' : '#ffffff'
})
Why updateOptions instead of a full recreate: Recreating the chart clears all internal state including zoom level, hidden series, and animation state. updateOptions is an in-place update, so the user experience is seamless. It is also faster since the chart skips any initialization work.
Following OS preference automatically
The browser exposes the user's OS preference through the prefers-color-scheme media query. Read the initial value to set the right mode on first render, then listen for changes so the chart responds when the user toggles their system theme.
import ApexCharts from 'apexcharts'
const mq = window.matchMedia('(prefers-color-scheme: dark)')
const el = document.querySelector('#chart')
const options = {
chart: { type: 'line', height: 350 },
theme: { mode: mq.matches ? 'dark' : 'light' },
series: [{ name: 'Revenue', data: [30, 40, 35, 50, 49, 60, 70] }],
xaxis: { categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'] }
}
// Set container background to match the initial mode
el.style.background = mq.matches ? '#1a1a2e' : '#ffffff'
const chart = new ApexCharts(el, options)
chart.render()
// Update when the OS preference changes
mq.addEventListener('change', (e) => {
chart.updateOptions({ theme: { mode: e.matches ? 'dark' : 'light' } })
el.style.background = e.matches ? '#1a1a2e' : '#ffffff'
})
Why use mq.matches at construction time: If you only wire up the change listener, the chart starts in light mode regardless of the OS setting and only switches if the user toggles while the page is open. Reading mq.matches at construction time means users on a dark-mode system see a dark chart immediately on load.
About addEventListener('change', ...): The older .addListener() API is deprecated. Use addEventListener for future compatibility. Both pass a MediaQueryListEvent whose .matches property reflects the new state.
React: syncing with state
In React, store the current mode in state and derive the theme.mode option from it. ReactApexChart will call updateOptions internally when the options prop changes, so no imperative chart calls are needed.
'use client'
import { useState, useEffect } from 'react'
import ReactApexChart from 'react-apexcharts'
import type { ApexOptions } from 'apexcharts'
export default function ThemedChart() {
const [isDark, setIsDark] = useState(false)
useEffect(() => {
const mq = window.matchMedia('(prefers-color-scheme: dark)')
setIsDark(mq.matches)
const handler = (e: MediaQueryListEvent) => setIsDark(e.matches)
mq.addEventListener('change', handler)
return () => mq.removeEventListener('change', handler)
}, [])
const options: ApexOptions = {
chart: { type: 'line' },
theme: { mode: isDark ? 'dark' : 'light' }
}
return (
<div
style={{
background: isDark ? '#1a1a2e' : '#ffffff',
borderRadius: 8,
padding: 16
}}
>
<ReactApexChart
type="line"
series={[{ name: 'Revenue', data: [30, 40, 35, 50, 49, 60] }]}
options={options}
height={350}
/>
</div>
)
}
Why useState initializes to false: Server-side rendering does not have access to window.matchMedia. Initializing to false (light mode) avoids a hydration mismatch. The useEffect runs after mount and sets the correct value, producing at most one additional render before the user sees anything.
Why the empty dependency array on useEffect: The listener should be registered once on mount and removed on unmount. Adding dependencies would restart the effect unnecessarily and could register duplicate listeners.
Next.js with next-themes
If your project uses next-themes for site-wide theme management, read resolvedTheme from the useTheme hook and map it to ApexCharts' theme.mode. The mounted guard is required to prevent hydration mismatch.
'use client'
import { useTheme } from 'next-themes'
import ReactApexChart from 'react-apexcharts'
import { useEffect, useState } from 'react'
export default function ThemedChart() {
const { resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
// Render nothing until mounted — avoids the hydration mismatch
// that would occur if the server guesses a theme incorrectly.
if (!mounted) return null
return (
<ReactApexChart
type="bar"
series={[{ name: 'Sales', data: [44, 55, 41, 67, 22] }]}
options={{
chart: { type: 'bar' },
theme: { mode: resolvedTheme === 'dark' ? 'dark' : 'light' }
}}
height={350}
/>
)
}
Why resolvedTheme instead of theme: The theme value from useTheme can be 'system', which is not a valid theme.mode value. resolvedTheme is always the actual resolved value ('dark' or 'light'), never 'system'. Use it whenever you need the concrete current value.
Why return null before mounted: During server-side rendering, next-themes does not know the user's OS preference or stored preference. If you render the chart before mounting, the server picks a default theme and the client corrects it, causing a visible flash and a React hydration warning. Returning null until the component is mounted on the client avoids both.
Customizing series colors for dark backgrounds
theme.mode: 'dark' adjusts structural colors (labels, lines, tooltip background) but leaves your series colors unchanged. Colors that look good on a white background are often too muted on dark. Pass a colors array alongside the theme change to swap in a brighter palette.
chart.updateOptions({
theme: { mode: 'dark' },
colors: ['#00E396', '#008FFB', '#FEB019']
})
To reset back to light mode with the original palette:
chart.updateOptions({
theme: { mode: 'light' },
colors: ['#008FFB', '#00E396', '#FEB019']
})
Series colors are not part of the theme system — they are plain config values. This means you are always in control of the palette and ApexCharts will never override it automatically when the mode changes.
Exporting dark mode charts
dataURI() captures the chart SVG. Because the container background is CSS and not part of the SVG, the exported image will have a transparent (or white, depending on the export target) background, not the dark background you see on screen.
To include the dark background in exports, set chart.background in the options. This applies a fill to the SVG root element itself, which is captured by dataURI().
const options = {
chart: {
type: 'line',
height: 350,
background: '#1a1a2e' // baked into the SVG, visible in exports
},
theme: { mode: 'dark' }
// ...
}
When using chart.background, you can remove the CSS background from the container since both the on-screen SVG and the export will show the same color. If you need the container to have a slightly different padding or border-radius appearance, you can keep both.
Limitations
theme.modecontrols only internal chart element colors. Container background, scrollbars, surrounding page chrome, and any overlay elements you add are unaffected.- There is no built-in two-way binding between
theme.modeandchart.background. If you set both, you must keep them synchronized manually when toggling modes. - Series colors are not remapped by
theme.mode. Bright colors that read well on dark may need a manual palette swap (see Customizing series colors above). theme.monochromeis independent of dark mode. You can combine both:theme: { mode: 'dark', monochrome: { enabled: true, color: '#00E396' } }produces a monochrome palette rendered in dark mode.