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 also paints the chart's own background, #343A3F, unless you set chart.background. To keep the page surface under your control, set chart.background: 'transparent' and style the container in CSS.


Follow the OS automatically with Facet (ApexCharts 6.0)

ApexCharts 6.0 can track the operating system's light and dark preference with no toggle code. Import the Facet feature and set theme.follow: 'os':

import ApexCharts from 'apexcharts'
import 'apexcharts/features/facet'

const options = { theme: { follow: 'os' } }

You can also drive the chart's colors from CSS design tokens (--apx-*), so it re-themes together with the rest of your page, and reference named brand themes registered with ApexCharts.registerTheme. See the theme options and the Themes guide. The manual theme.mode approach below still works and is the right choice when you own the toggle yourself.

Basic dark mode chart

Pass theme.mode: 'dark' when constructing the chart, set chart.background: 'transparent', and give the container the dark background you want. Without the background line the chart paints its own #343A3F surface, which shows as a grey box inside any container that is a different dark.

<div id="chart"></div>
#chart {
  background: #1a1a2e;
  border-radius: 8px;
  padding: 16px;
}
import ApexCharts from 'apexcharts'

const options = {
  chart: {
    type: 'line',
    height: 350,
    background: 'transparent' // let the container's color show through
  },
  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 set the background explicitly: when chart.background is unset, the mode chooses one for you. theme.mode: 'dark' paints the SVG #343A3F and 'light' paints it #fff. That is a sensible default for a chart on its own, but on a designed page the chart should sit on your surface, not its own. 'transparent' hands the background back to CSS, and from then on the container's color is the chart's color in both modes.


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, background: 'transparent' },
  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, background: 'transparent' },
  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', background: 'transparent' },
    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

An export cannot see your container's CSS background. When chart.background is unset or 'transparent', a dark-mode export is filled with the chart's default dark surface, #343A3F, so a chart that sits on #1a1a2e on screen exports onto a lighter grey.

To export onto the same color you see on screen, set chart.background to that color. It fills the SVG root itself, which is what dataURI() captures.

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.mode controls only internal chart element colors. Container background, scrollbars, surrounding page chrome, and any overlay elements you add are unaffected.
  • theme.mode only chooses the background while chart.background is unset. Once you set a color there, it stays that color in both modes, so a toggle has to update both. 'transparent' is the exception that works in both modes, because the container supplies the color.
  • 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.monochrome is independent of dark mode. You can combine both: theme: { mode: 'dark', monochrome: { enabled: true, color: '#00E396' } } produces a monochrome palette rendered in dark mode.