Server side rendering

ApexCharts fully supports server-side rendering for Next.js, Nuxt, SvelteKit, Astro, and other modern meta-frameworks. Charts rendered on the server produce static SVG that is immediately visible to users and web crawlers, with full interactivity restored on the client via hydration.

Package Entry Points

apexcharts/ssr covers both halves of the workflow: it renders charts on the server and hydrates them in the browser, so import it on both sides. The other two entries give the browser the default bundle, which has no SSR or hydration methods:

ImportEnvironmentWhat it provides
apexcharts/ssrNode.js and browserrenderToString() and renderToHTML() on the server; hydrate(), hydrateAll() and isHydrated() in the browser
apexcharts/clientBrowserThe browser ESM build of the default bundle, with no SSR or hydration methods
apexchartsNode.js and browserIn Node.js it resolves to the SSR build. In a browser bundle it resolves to the default bundle, with no SSR or hydration methods

The type definitions declare all five methods on the one ApexCharts class, so TypeScript accepts hydrate() on apexcharts/client or on apexcharts in browser code, and the call then fails in the browser with a hydrate is not a function TypeError.

In the browser apexcharts/ssr carries its own copy of the chart core. A page that also imports apexcharts, or an opt-in add-on such as apexcharts/sunburst (add-ons import apexcharts/core), ships a second copy.

Server-Side Rendering

Use renderToHTML() to generate hydration-ready HTML. It embeds the chart configuration in the output so the client can fully restore interactivity without re-fetching data. When the chart shows a legend, the legend and its stylesheet come with the SVG, so the chart is complete before hydration runs, and where it never runs.

// server.js (Node.js / any SSR framework)
import ApexCharts from 'apexcharts/ssr'

const html = await ApexCharts.renderToHTML(
  {
    series: [{ name: 'Sales', data: [30, 40, 35, 50, 49, 60, 70] }],
    chart:  { type: 'bar' },
    xaxis:  { categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'] }
  },
  { width: 600, height: 350 }
)

// html is a self-contained string ready to embed in your page:
// <div class="apexcharts-ssr-wrapper" data-apexcharts-hydrate
//      data-apexcharts-config="...">
//   <svg ...>...</svg>
// </div>

renderToHTML() Options

OptionTypeDefaultDescription
widthnumber400Chart width in pixels
heightnumber300Chart height in pixels
scalenumber1SVG scale factor (useful for high-DPI output)
classNamestring''Additional CSS class added to the wrapper div

renderToString(): SVG Only

Use renderToString() when you only need the raw SVG string, for example to save to a file, pipe to a PDF renderer, or embed inside your own wrapper element.

import ApexCharts from 'apexcharts/ssr'

const svg = await ApexCharts.renderToString(
  {
    series: [{ data: [10, 41, 35, 51, 49, 62, 69, 91, 148] }],
    chart:  { type: 'line' }
  },
  { width: 800, height: 400 }
)

// svg is a plain SVG string
// '<svg xmlns="http://www.w3.org/2000/svg" ...>...</svg>'

The SVG has no legend. The legend is an HTML element beside the SVG, which a standalone SVG cannot carry, so the plot fills the whole chart. Use renderToHTML() for a chart that needs its legend; its output works without hydration too.

Chart Types and Features Outside the Default Bundle

apexcharts/ssr renders the chart types in the default bundle. Since 8.0 that bundle leaves out unit (with waffle and beeswarm), sunburst and violin, and the drilldown, waterfall, dumbbell and streamgraph features. Each takes the same side-effect import next to apexcharts/ssr that it takes next to apexcharts:

import ApexCharts from 'apexcharts/ssr'
import 'apexcharts/sunburst'

const html = await ApexCharts.renderToHTML(
  {
    chart:  { type: 'sunburst' },
    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 }] }
      ]
    }]
  },
  { width: 500, height: 500 }
)

A feature is apexcharts/features/<name>, for example apexcharts/features/waterfall. Icicle, raincloud and the features that were opt-in before 8.0 work the same way; the tree-shaking guide lists them. import 'apexcharts/full' next to apexcharts/ssr registers every chart type and feature at once.

On the server a missing chart type does not produce an empty chart. renderToHTML() and renderToString() reject with SSR rendering failed: followed by the line that names the import. A missing feature logs a warning and renders the chart without it.

Hydration builds the chart again in the browser from the embedded config, so the code that hydrates needs the same import next to apexcharts/ssr: import 'apexcharts/sunburst' in that module, or, when you load the hydration build lazily, await import('apexcharts/sunburst') before calling hydrate() or hydrateAll(). Without it the browser logs that the chart type is not registered and the chart keeps its static SVG.

A trellis chart (apexcharts/features/trellis) draws its panels in the browser only. On the server it logs a warning and renders an empty SVG of the requested size with the class apexcharts-ssr-placeholder, and hydration draws the panels.

Client-Side Hydration

Once the server-rendered HTML is in the browser, call hydrate() or hydrateAll() to restore full interactivity: tooltips, zoom, pan, legend toggling, and all other interactions.

// client.js (browser only)
import ApexCharts from 'apexcharts/ssr'

// Hydrate a single chart
const el = document.getElementById('my-chart')
const chart = ApexCharts.hydrate(el)

// Or hydrate every chart on the page at once
const charts = ApexCharts.hydrateAll()

Both return straight away, before the charts have rendered. To confirm that a chart is interactive, use isHydrated() or the apexcharts:hydrated event, both covered below.

Hydrate All on DOM Ready

import ApexCharts from 'apexcharts/ssr'

document.addEventListener('DOMContentLoaded', () => {
  ApexCharts.hydrateAll()
})

hydrateAll() with a Custom Selector

By default hydrateAll() targets every element with data-apexcharts-hydrate. Pass a CSS selector to restrict hydration to a subset of elements.

import ApexCharts from 'apexcharts/ssr'

// Only hydrate charts inside the dashboard section
ApexCharts.hydrateAll('#dashboard [data-apexcharts-hydrate]')

Applying Client-Side Overrides on Hydration

Pass a clientOptions object as the second argument to override parts of the SSR configuration when hydrating. Common uses: tuning animations, adding event handlers, or changing theme for the interactive version.

import ApexCharts from 'apexcharts/ssr'

ApexCharts.hydrateAll('[data-apexcharts-hydrate]', {
  chart: {
    animations: { enabled: true, speed: 400 }
  },
  theme: { mode: 'dark' }
})

Hydration builds the chart from the embedded configuration, with clientOptions on top. The configuration travels as JSON, so functions such as formatters, chart.events handlers and tooltip.custom do not survive it: pass them in clientOptions. The server switches animations and the toolbar off for its own output only, so a chart that leaves them unset has both on once hydrated. A chart whose options set chart.animations.enabled: false or chart.toolbar.show: false keeps them off in the browser too (before 8.1, hydration turned both back on). To have them in the browser, pass them in clientOptions, as above; clientOptions can switch them off as well.

clientOptions replaces each top-level key whole, except chart, which it merges one level deep. So chart.animations above replaces the embedded chart.animations object, and a setting such as speed that it does not repeat falls back to its default. In the same way, a theme in clientOptions replaces the embedded theme.

Checking Hydration State

isHydrated() returns true once the chart has finished rendering, just before the apexcharts:hydrated event below fires. Until then it returns false, including right after a hydrate() call.

import ApexCharts from 'apexcharts/ssr'

const el = document.getElementById('my-chart')

if (ApexCharts.isHydrated(el)) {
  console.log('Chart is already interactive')
} else {
  ApexCharts.hydrate(el)
}

Hydration Events

Each element dispatches an apexcharts:hydrated custom event when hydration completes successfully. Listen to it if you need to run code after the chart is interactive. The event does not bubble, so add the listener to the chart element itself.

This event and isHydrated() are the reliable checks. The array from hydrateAll() is not: it also holds an instance for a chart that fails to render, which leaves that element with its static SVG and logs ApexCharts hydration failed: in the console.

import ApexCharts from 'apexcharts/ssr'

const el = document.getElementById('my-chart')

el.addEventListener('apexcharts:hydrated', (e) => {
  const { chart } = e.detail   // the ApexCharts instance
  console.log('Chart ready:', chart)
})

ApexCharts.hydrate(el)

Next.js Example

// app/page.jsx (Server Component)
import ApexCharts from 'apexcharts/ssr'
import ClientHydrator from './ClientHydrator'

export default async function Page() {
  const chartHTML = await ApexCharts.renderToHTML(
    {
      series: [{ name: 'Monthly Revenue', data: [31, 40, 28, 51, 42, 109, 100] }],
      chart:  { type: 'area' },
      xaxis:  { categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'] }
    },
    { width: 700, height: 350 }
  )

  return (
    <main>
      <h1>Dashboard</h1>
      <div dangerouslySetInnerHTML={{ __html: chartHTML }} />
      <ClientHydrator />
    </main>
  )
}
// app/ClientHydrator.jsx
'use client'
import { useEffect } from 'react'

export default function ClientHydrator() {
  useEffect(() => {
    let charts = []
    let cancelled = false

    // useEffect runs only in the browser, so the hydration build loads there
    import('apexcharts/ssr').then(({ default: ApexCharts }) => {
      if (!cancelled) charts = ApexCharts.hydrateAll()
    })

    return () => {
      cancelled = true
      charts.forEach((chart) => chart.destroy())
    }
  }, [])

  return null
}

The cleanup destroys the charts when the component unmounts, and the cancelled flag covers an unmount that happens before the import resolves.

Nuxt Example

// server/api/chart.js
import ApexCharts from 'apexcharts/ssr'

export default defineEventHandler(async () => {
  const html = await ApexCharts.renderToHTML(
    {
      series: [{ data: [10, 41, 35, 51, 49, 62, 69] }],
      chart:  { type: 'line' }
    },
    { width: 600, height: 300 }
  )
  return { html }
})
<!-- pages/index.vue -->
<template>
  <div v-html="chartHtml" />
</template>

<script setup>
const { data } = await useFetch('/api/chart')
const chartHtml = data.value.html

let charts = []
let unmounted = false

// onMounted runs only in the browser, so the hydration build loads there
onMounted(async () => {
  const { default: ApexCharts } = await import('apexcharts/ssr')
  if (!unmounted) charts = ApexCharts.hydrateAll()
})

onBeforeUnmount(() => {
  unmounted = true
  charts.forEach((chart) => chart.destroy())
})
</script>

vue3-apexcharts (<apexchart-hydrate>) and ng-apexcharts hydrate the same way: they load apexcharts/ssr with a dynamic import once the component is in the browser, and destroy the charts when it is removed. The Nuxt guide shows the components.

How the HTML Output Is Structured

renderToHTML() returns a wrapper div containing the static SVG and a base64-encoded copy of the chart configuration. The client uses the encoded config to reconstruct the fully interactive chart on hydration, no server round-trip needed.

<div class="apexcharts-ssr-wrapper"
     data-apexcharts-hydrate
     data-apexcharts-config="eyJzZXJpZXMiOi4uLn0=">
  <svg xmlns="http://www.w3.org/2000/svg" ...>
    <!-- full chart SVG -->
  </svg>
</div>

When the chart shows a legend, the SVG and the legend sit in a box sized to the chart, with the legend's stylesheet between them:

<div class="apexcharts-ssr-wrapper" data-apexcharts-hydrate data-apexcharts-config="...">
  <div style="position:relative;width:600px;height:350px">
    <svg xmlns="http://www.w3.org/2000/svg" ...>...</svg>
    <style>/* legend styles */</style>
    <div class="apexcharts-legend ...">...</div>
  </div>
</div>

The <style> tag carries chart.nonce when you set one, and chart.injectStyleSheet: false leaves it out. A single-series chart has no legend unless legend.showForSingleSeries is on, and legend.show: false keeps the SVG-only output above for any chart.

After hydration, the element receives data-apexcharts-hydrated="true" and the data-apexcharts-config attribute is removed.

Rendering Data You Do Not Control

Server output escapes the text and attribute values it writes into the SVG, so a series name, category or title such as Tom & Jerry or <img src=x onerror=...> reaches the SVG as text. This needs ApexCharts 8.0.1 or later: 8.0.0 wrote them verbatim, which broke the SVG or emitted live markup. Upgrade before rendering data you do not control.

The legend and the tooltip are the exceptions, because they write HTML. The legend writes its entries as HTML, in the browser and in the legend renderToHTML() emits: series names, the labels of a pie or donut, the categories of a distributed bar chart, the range names of a heatmap's color scale, and legend.customLegendItems. Once the chart hydrates, the tooltip writes series names and categories as HTML too, as soon as the reader hovers, even with the legend hidden. Remove markup from any of these that come from data you do not control before they reach the chart.