# How to Add Interactive Charts to Astro with ApexCharts

Source: https://dteather.com/blogs/astro-interactive-charts/
Author: David Teather
Published: 2024-10-30
Updated: 2026-07-05
Description: Add interactive ApexCharts to Astro Markdown with a custom rehype plugin, dark-mode syncing, and a chart code block workflow.
Categories: tutorials
Agent context: This is the Markdown source for a technical blog post. Use the source URL for citation, and use the HTML page when interactive demos, charts, images, or videos are relevant.

I wanted charts in Astro Markdown without turning the whole page into a React island, so I ended up wiring [ApexCharts](https://apexcharts.com/) into a custom rehype flow. The result is pretty clean: you write a `chart` code block with JSON in Markdown, it renders as an interactive chart, and it follows the site's dark mode.

This is the exact setup this site uses, so every chart in this post is live. Hover over this one to see what you're building:

```chart
{
  "series": [
    {
      "name": "TEAM A",
      "type": "column",
      "data": [
        23,
        11,
        22,
        27,
        13,
        22,
        37,
        21,
        44,
        22,
        30
      ]
    },
    {
      "name": "TEAM B",
      "type": "area",
      "data": [
        44,
        55,
        41,
        67,
        22,
        43,
        21,
        41,
        56,
        27,
        43
      ]
    },
    {
      "name": "TEAM C",
      "type": "line",
      "data": [
        30,
        25,
        36,
        30,
        45,
        35,
        64,
        52,
        59,
        36,
        39
      ]
    }
  ],
  "chart": {
    "height": 350,
    "type": "line",
    "stacked": false
  },
  "stroke": {
    "width": [
      0,
      2,
      5
    ],
    "curve": "smooth"
  },
  "plotOptions": {
    "bar": {
      "columnWidth": "50%"
    }
  },
  "fill": {
    "opacity": [
      0.85,
      0.25,
      1
    ],
    "gradient": {
      "inverseColors": false,
      "shade": "light",
      "type": "vertical",
      "opacityFrom": 0.85,
      "opacityTo": 0.55,
      "stops": [
        0,
        100,
        100,
        100
      ]
    }
  },
  "labels": [
    "01/01/2003",
    "02/01/2003",
    "03/01/2003",
    "04/01/2003",
    "05/01/2003",
    "06/01/2003",
    "07/01/2003",
    "08/01/2003",
    "09/01/2003",
    "10/01/2003",
    "11/01/2003"
  ],
  "markers": {
    "size": 0
  },
  "xaxis": {
    "type": "datetime"
  },
  "yaxis": {
    "title": {
      "text": "Points"
    }
  },
  "tooltip": {
    "shared": true,
    "intersect": false,
    "y": {}
  }
}
```

That one is from an [ApexCharts example](https://apexcharts.com/javascript-chart-demos/mixed-charts/line-column-area/), and it's defined entirely in this post's markdown. A simpler one looks like this

````
```chart
{
  "chart": {
    "type": "line"
  },
  "series": [{
    "name": "sales",
    "data": [30,40,35,50,49,60,70,91,125]
  }],
  "xaxis": {
    "categories": [1991,1992,1993,1994,1995,1996,1997, 1998,1999]
  }
}
```
````

which renders out as

```chart
{
  "chart": {
    "type": "line"
  },
  "series": [{
    "name": "sales",
    "data": [30,40,35,50,49,60,70,91,125]
  }],
  "xaxis": {
    "categories": [1991,1992,1993,1994,1995,1996,1997, 1998,1999]
  }
}
```

## What You Need

- An existing Astro site that renders markdown, content collections or plain markdown pages both work
- Node and npm
- Around 15 minutes

The dark mode section assumes you track your theme with a `data-theme` attribute on the html tag (the DaisyUI way), but it should be easy to adapt to however you do it. If your site doesn't have dark mode you can skip that section entirely and still get working charts.

## Why ApexCharts?

This is what I wanted and why I chose [ApexCharts](https://apexcharts.com/)

1. Interactive charts, tooltips, zooming, and legend toggling without writing any javascript per chart
2. Define the chart in Markdown, ApexCharts configs are plain JSON so they can live directly in a code block
3. Support darkmode, there's a built in dark theme so I don't have to hand pick colors

I also didn't want to ship a React island just to render a chart on an otherwise static page, and ApexCharts runs from a normal script tag. [Chart.js](https://www.chartjs.org/) would also work, but the theming is more manual. D3 gives you way more control, but it also turns each chart into a mini project. React-first libraries like Recharts were overkill for what I wanted here. The same general setup from this post would work with any of them; only the render step really changes.

The setup is three parts. A custom rehype plugin that turns `chart` code blocks into divs at build time, a small client side script that renders an ApexCharts chart into each div on page load, and a theme helper that applies dark or light mode and re-applies it when the theme toggles. This is the same code-block-to-component trick I used for [mermaid UML diagrams](./2024-10-06-astro-uml-diagrams.md) if you want more of it.

## Architecture Overview

| Step                | What happens                                                                                                                   |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Markdown code block | You write a `chart` code block with strict JSON in the post.                                                                   |
| Rehype plugin       | At build time, the plugin finds `language-chart` blocks, parses the JSON, and replaces the block with a `div.chart-container`. |
| Static HTML         | The rendered page ships a `div[data-chart]` with the serialized chart config attached.                                         |
| ApexCharts render   | A small client-side script reads `data-chart`, instantiates ApexCharts, and renders the interactive chart in place.            |
| Theme sync          | A second helper updates the chart theme when the site theme changes.                                                           |

## Initial Setup

### Integration With Rehype

Our first step is to tell our markdown, rehype in this case, to support these kinds of codeblocks.

````
```chart
{
    // our Apex Charts config
}
```
````

To get this done, we'll use a custom Rehype plugin. At build time it walks the html tree, finds `<pre><code class="language-chart">` blocks, and replaces each one with a single div that has the chart config stored in a `data-chart` attribute. It also generates a unique ID per chart which we'll need later to update charts when the theme changes. Here's what mine looks like, although my conditions might be different, since I'm accumulating a lot of these Rehype plugins.

```js
import { visit } from "unist-util-visit";
import crypto from "crypto";

export function rehypeChart() {
  return (tree) => {
    visit(tree, "element", (node) => {
      // The structure my markdown gets parsed as is <pre><code>...</code></pre>
      // and because of styling I want to replace the <pre><code> with just one <div>
      if (
        node.tagName === "pre" &&
        node.children.length === 1 &&
        node.children[0].tagName === "code"
      ) {
        const childNode = node.children[0];

        // check if the child node is a code block with the language set to 'chart'
        if (
          childNode.tagName === "code" &&
          childNode.properties.className &&
          childNode.properties.className.length > 0 &&
          childNode.properties.className[0] === "language-chart"
        ) {
          // Check if the code block contains only text
          if (
            childNode.children.length !== 1 ||
            childNode.children[0].type !== "text"
          ) {
            throw new Error("Invalid chart code block");
          }

          // Parse the chart's data from the code block
          var chartData = JSON.parse(childNode.children[0].value);
          // Generate a unique ID for the chart, this will let us reference the chart later for dark mode
          const uuid = crypto.randomBytes(16).toString("hex");
          chartData.chart.id = uuid;

          // Replace the node with a div element with the chart data attached
          node.type = "element";
          node.tagName = "div";
          node.properties = {
            id: uuid,
            className: ["chart-container"],
            "data-chart": JSON.stringify(chartData),
          };
          node.children = [];
        }
      }
    });
  };
}
```

The reason we replace the `<pre><code>` pair with just one div is so your code block styling doesn't bleed into the chart containers.

Then we can use this function in our astro config which should look like something like this

```js title="astro.config.mjs"
export default defineConfig({
  markdown: {
    rehypePlugins: [rehypeChart], // add to your existing plugins array
  },
});
```

### Adding The Javascript

From that, we'll just get a div without any interactive components to it (or a cool visualization). Next, we'll add javascript so that the chart actually renders out.

First install ApexCharts to your project with

```
npm i apexcharts
```

Then, in your page where you render out the markdown in my case `ArticleLayout.astro`, add the following. It finds every chart container on the page, parses the config back out of the data attribute, and renders a chart into it.

```astro
---
<script>
  import ApexCharts from "apexcharts";

  // when the page loads
  document.addEventListener("DOMContentLoaded", () => {
    // look for each of our charts
    document.querySelectorAll(".chart-container").forEach((chartDiv) => {
  // extract our ApexCharts config and render it out
      const chartDataAttr = chartDiv.getAttribute("data-chart");
      if (chartDataAttr) {
        const chartData = JSON.parse(chartDataAttr);
        const chart = new ApexCharts(
          chartDiv,
          chartData
        );
        chart.render();
      }
    });
  });
</script>
```

With this you should be able to get your charts working and maybe try out some of the [examples](https://apexcharts.com/javascript-chart-demos/) or just use the simple one from earlier with

````
```chart
{
  "chart": {
    "type": "line"
  },
  "series": [{
    "name": "sales",
    "data": [30,40,35,50,49,60,70,91,125]
  }],
  "xaxis": {
    "categories": [1991,1992,1993,1994,1995,1996,1997, 1998,1999]
  }
}
```
````

However the styling isn't that great and I wanted to support dark-mode.

## Supporting Darkmode

Luckily the charts have a dark theme. I found this nice function on a GitHub issues page that allows us to take our config and conditionally apply the dark mode or not.

```js
function flipChartThemeMode(chartOptions: any, darkMode: boolean) {
  const theme = chartOptions.theme;
  const tooltip = chartOptions.tooltip;
  const chart = chartOptions.chart;

  chartOptions = {
    ...chartOptions,
    chart: {
      ...chart,
      background: "rgba(0, 0, 0, 0)", // also make the background transparent
      fontFamily: "inherit", // inherit the font family
    },
    theme: {
      ...theme,
      mode: darkMode ? "dark" : "light",
    },
    tooltip: {
      ...tooltip,
      theme: darkMode ? "dark" : "light",
    },
  };

  return chartOptions;
}
```

Then I updated our function from earlier to this for the initialization of the page. It reads the current theme and passes every config through that helper before rendering.

```js {6-7,16}
<script>
  import ApexCharts from "apexcharts";
  import { flipChartThemeMode } from "../utils/theme-helpers"

  document.addEventListener("DOMContentLoaded", () => {
    // this is how I track my theme, with DaisyUI
    const dataTheme = document.documentElement.getAttribute("data-theme");

    document.querySelectorAll(".chart-container").forEach((chartDiv) => {
      const chartDataAttr = chartDiv.getAttribute("data-chart");
      if (chartDataAttr) {
        const chartData = JSON.parse(chartDataAttr);

        const chart = new ApexCharts(
          chartDiv,
          flipChartThemeMode(chartData, dataTheme === "dark")
        );
        chart.render();
      }
    });
  });
</script>
```

That just handles page load though, so if someone flips the theme toggle mid read the charts would stay in the old theme. To fix that I added this segment to my function that is triggered when the theme should switch. It looks each chart back up by the ID our rehype plugin generated and updates its options in place.

```js
// inside of an existing function called toggleTheme()
// this will depend if you also have a toggle for your theme
document.querySelectorAll(".chart-container").forEach((chartDiv) => {
  const chartDataAttr = chartDiv.getAttribute("data-chart");
  if (chartDataAttr) {
    const chartData = JSON.parse(chartDataAttr);
    const chartID = chartDiv.firstElementChild?.id; // uses our ID from earlier
    if (!chartID) return;
    const chart = ApexCharts.getChartByID(chartID.replace("apexcharts", ""));
    if (chart) {
      chart.updateOptions(flipChartThemeMode(chartData, newTheme === "dark"));
    } else {
      console.error("chart not found");
    }
  }
});
```

Using `updateOptions` re-renders the chart with the new theme without destroying and recreating it, so the swap is instant.

Finally I added the following global css. I'm using tailwind prose to format my markdown and it has some builtin colors/styles that I wanted to keep the charts the style in. So feel free to change to fit your design

```css
.apexcharts-text {
  fill: var(--tw-prose-headings) !important;
  color: var(--tw-prose-headings) !important;
}

.apexcharts-tooltip {
  color: var(--tw-prose-headings) !important;
}

.apexcharts-xaxistooltip {
  color: var(--tw-prose-headings) !important;
  background-color: transparent !important;
  display: none; /* I wanted to hide the x-axis tooltip */
}

.apexcharts-legend-text {
  color: var(--tw-prose-headings) !important;
}
```

I'm not totally happy with the tooltip background colors at the moment but it fits well enough for now. If you have any better css for tailwind prose please shoot them my way and I'll update the blog post!

## Troubleshooting

Some things that got me, or might get you:

- The config has to be strict JSON since `JSON.parse` runs at build time. A trailing comma, a comment, or unquoted keys will fail your build. The ApexCharts docs show javascript object literals not JSON, so if you copy one of their examples you'll need to quote the keys.
- Every config needs a `"chart"` object since the rehype plugin sets `chartData.chart.id`. Even an empty `"chart": {}` is enough.
- If you're using Astro's view transitions, `DOMContentLoaded` only fires on the first full page load so charts won't render after client side navigation. Listen for `astro:page-load` instead and the same init code works.
- If the theme toggle logs "chart not found", the toggle probably ran before the chart finished its initial render, or the `chart.id` never got set.
- Apex charts measures its container when it renders, so a chart inside a collapsed accordion or hidden tab will render at zero width. Render it once the container is actually visible.

Once it's all working some easy customizations: hide the toolbar with `"chart": { "toolbar": { "show": false } }` for a cleaner look in posts, set a consistent `"height"` across charts so your article layout doesn't jump around, or extend `flipChartThemeMode` to pull your brand colors from css variables so every chart matches your palette automatically.

And that's it, hopefully now you can use charts in Astro. If you're building out an Astro blog I've also written about [adding a search bar](./2024-7-26-2024-astro-search-bar.md), [optimizing your images](./2024-7-26-astro-optimized-images.md), and [pulling live stats into a static site](./2025-08-22-how-to-add-live-stats-to-your-site.md).

If you found this helpful or want more Astro tutorial content, let me know.
