Building a Dashboard Layout

ApexCharts makes it straightforward to compose multiple charts into a cohesive dashboard: KPI tiles using sparklines, a main time-series chart, and secondary breakdown charts that synchronize their tooltips and zoom with the main chart. This guide covers the full layout pattern from HTML structure through synchronization, loading states, responsive sizing, and a React equivalent.

HTML layout with CSS Grid

The outer .dashboard wrapper uses CSS Grid so the KPI row, main chart, and secondary charts stack naturally and collapse to a single column on small screens.

<div class="dashboard">
  <!-- KPI row -->
  <div class="kpi-row">
    <div class="kpi-card">
      <div id="kpi-revenue"></div>
      <span>Revenue</span>
    </div>
    <div class="kpi-card">
      <div id="kpi-users"></div>
      <span>Active Users</span>
    </div>
    <div class="kpi-card">
      <div id="kpi-conversion"></div>
      <span>Conversion</span>
    </div>
  </div>

  <!-- Main chart -->
  <div id="main-chart"></div>

  <!-- Secondary charts row -->
  <div class="chart-row">
    <div id="chart-region"></div>
    <div id="chart-category"></div>
  </div>
</div>
.dashboard {
  display: grid;
  gap: 24px;
  padding: 24px;
}

.kpi-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}

.kpi-card {
  background: #f9fafb;
  border-radius: 8px;
  padding: 16px;
}

.chart-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}

@media (max-width: 768px) {
  .kpi-row  { grid-template-columns: 1fr; }
  .chart-row { grid-template-columns: 1fr; }
}

The media query collapses the three-column KPI row and the two-column chart row into a single column on screens narrower than 768 px. Charts themselves also respond — see the Responsive chart height section below.

Shared global options with window.Apex

Before creating any chart instance, assign default options to window.Apex. ApexCharts reads this object as a baseline; individual chart options override it, so you only have to set shared defaults once.

window.Apex = {
  chart: {
    toolbar: { show: false },
    zoom: { enabled: false }
  },
  dataLabels: { enabled: false }
}

Setting toolbar.show: false globally removes the download/zoom/pan toolbar from every chart. Setting zoom.enabled: false prevents accidental zoom on charts that do not need it. Both can be re-enabled on individual charts by specifying chart: { zoom: { enabled: true } } in that chart's own options.

KPI sparklines

chart.sparkline.enabled: true strips axes, labels, padding, and the toolbar from a chart. The result is a minimal inline sparkline that fits inside a KPI tile. Three common sparkline styles are shown below.

// Area sparkline — shows trend direction
const kpiRevenue = new ApexCharts(document.querySelector('#kpi-revenue'), {
  chart: {
    type: 'area',
    height: 80,
    sparkline: { enabled: true }
  },
  series: [{ data: [31, 40, 28, 51, 42, 85, 77] }],
  colors: ['#008FFB'],
  stroke: { curve: 'smooth', width: 2 },
  fill: { opacity: 0.3 }
})

// Bar sparkline — emphasises individual periods
const kpiUsers = new ApexCharts(document.querySelector('#kpi-users'), {
  chart: {
    type: 'bar',
    height: 80,
    sparkline: { enabled: true }
  },
  series: [{ data: [12, 16, 9, 14, 11, 19, 22] }],
  colors: ['#00E396'],
  plotOptions: { bar: { borderRadius: 2 } }
})

// Radial progress — shows a percentage toward a goal
const kpiConversion = new ApexCharts(document.querySelector('#kpi-conversion'), {
  chart: {
    type: 'radialBar',
    height: 80,
    sparkline: { enabled: true }
  },
  series: [67],
  colors: ['#FEB019'],
  plotOptions: {
    radialBar: {
      hollow: { size: '65%' },
      dataLabels: { show: false }
    }
  }
})

kpiRevenue.render()
kpiUsers.render()
kpiConversion.render()

Sparkline series data is plain numbers — no x/timestamp values are needed. ApexCharts distributes the points evenly across the available width.

Synchronized charts

When multiple charts in a dashboard share the same time range, hovering over one chart should show the corresponding data point on all others. ApexCharts handles this with two options:

  • chart.id — a unique string identifier for the chart instance on this page
  • chart.group — charts with the same group string synchronize their tooltips and zoom
// The same `group` string wires tooltip and zoom sync across both charts
const mainChart = new ApexCharts(document.querySelector('#main-chart'), {
  chart: {
    id: 'main',
    group: 'dashboard',
    type: 'area',
    height: 300
  },
  series: [{ name: 'Revenue', data: [
    [1672531200000, 4200],
    [1675209600000, 5800],
    [1677628800000, 5100],
    [1680307200000, 6900],
    [1682899200000, 7400],
    [1685577600000, 6300]
  ]}],
  xaxis: { type: 'datetime' },
  yaxis: { labels: { minWidth: 50 } }  // must match across all grouped charts
})

const regionChart = new ApexCharts(document.querySelector('#chart-region'), {
  chart: {
    id: 'region',
    group: 'dashboard',
    type: 'bar',
    height: 200
  },
  series: [{ name: 'By Region', data: [
    [1672531200000, 1800],
    [1675209600000, 2200],
    [1677628800000, 1900],
    [1680307200000, 2600],
    [1682899200000, 2900],
    [1685577600000, 2400]
  ]}],
  xaxis: { type: 'datetime' },
  yaxis: { labels: { minWidth: 50 } }  // same minWidth as mainChart
})

mainChart.render()
regionChart.render()

Why yaxis.labels.minWidth matters

When charts are arranged in a vertical stack, the y-axis label column on the left acts as the alignment edge for the plot area. If that column is different widths across charts, the plot areas will not line up horizontally — the gridlines will appear offset. Setting minWidth to the same value on every grouped chart forces the label column to be identical, so the plot areas align precisely.

Zoom synchronization

Because chart.group is set, zooming on any chart in the group zooms all others to the same x range. This is useful when a main area chart and a secondary bar chart share the same time axis: the user can zoom into a period of interest and both charts update together. To disable zoom entirely, set chart.zoom.enabled: false in window.Apex or in the individual chart options.

Loading states

Render the chart immediately with an empty series and a noData.text message, then call updateSeries once the data arrives. This avoids a blank container flash and gives users visual feedback that content is on its way.

const chart = new ApexCharts(document.querySelector('#main-chart'), {
  chart: { type: 'line', height: 350 },
  series: [],
  noData: {
    text: 'Loading...',
    align: 'center',
    verticalAlign: 'middle',
    style: { fontSize: '16px' }
  }
})

await chart.render()

// Fetch data and update once it arrives
const data = await fetchDashboardData()
chart.updateSeries([{ name: 'Revenue', data: data.revenue }])

noData.text is cleared automatically as soon as updateSeries provides at least one data point.

Responsive chart height

CSS Grid handles column layout, but the chart height is specified in JavaScript and does not change with the container width. Use the responsive array to override chart options at specific breakpoints. The breakpoint value matches viewport widths narrower than that pixel count.

responsive: [
  {
    breakpoint: 768,
    options: {
      chart: { height: 200 },
      legend: { position: 'bottom' }
    }
  }
]

On viewports narrower than 768 px the chart renders at 200 px instead of its default height, and the legend moves below the chart to reclaim horizontal space. chart.redrawOnWindowResize defaults to true, so the chart redraws automatically when the window is resized — no extra resize listener is needed.

React dashboard pattern

In React, use ReactApexChart for each chart and hold series data in component state. Pass shared configuration through a plain object rather than through window.Apex so that each chart stays declarative and re-renders correctly when state changes.

import ReactApexChart from 'react-apexcharts'
import { useState } from 'react'

function Dashboard({ initialData }) {
  const [revenue, setRevenue] = useState(initialData.revenue)
  const [users, setUsers] = useState(initialData.users)

  const sharedOptions = {
    chart: {
      toolbar: { show: false },
      zoom: { enabled: false }
    },
    dataLabels: { enabled: false }
  }

  return (
    <div className="dashboard-grid">
      <ReactApexChart
        type="area"
        height={300}
        series={[{ name: 'Revenue', data: revenue }]}
        options={{
          ...sharedOptions,
          chart: {
            ...sharedOptions.chart,
            id: 'revenue',
            group: 'dash'
          },
          yaxis: { labels: { minWidth: 50 } }
        }}
      />
      <ReactApexChart
        type="bar"
        height={300}
        series={[{ name: 'Users', data: users }]}
        options={{
          ...sharedOptions,
          chart: {
            ...sharedOptions.chart,
            id: 'users',
            group: 'dash'
          },
          yaxis: { labels: { minWidth: 50 } }
        }}
      />
    </div>
  )
}

The spread { ...sharedOptions.chart, id: 'revenue', group: 'dash' } is necessary because replacing the chart key entirely would discard the shared toolbar and zoom settings. Spreading keeps the base and adds the per-chart overrides.

For data updates, call setRevenue or setUsers with the new array. ReactApexChart diffs the new series against the previous one and calls updateSeries internally.

Performance tips for dashboards with many charts

Disable entrance animations

With five or more charts on the page, entrance animations stagger the initial render and make the page feel slow. Turn them off globally:

window.Apex = {
  chart: {
    animations: { enabled: false },
    toolbar: { show: false }
  }
}

Individual charts that benefit from animation (for example, a single large feature chart) can re-enable it with animations: { enabled: true } in their own options.

Use sparklines for KPI tiles

chart.sparkline.enabled: true skips the axis, label, and toolbar rendering pipeline. On a dashboard with a dozen KPI tiles, sparklines reduce the total render work compared to full charts cropped by CSS.

Render charts lazily with IntersectionObserver

Charts below the fold do not need to render until the user scrolls to them. Deferring render() until the container is visible saves work on the initial page load.

function renderWhenVisible(el, options) {
  const chart = new ApexCharts(el, options)

  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        chart.render()
        observer.unobserve(el)  // render once, then stop observing
      }
    })
  })

  observer.observe(el)
  return chart
}

// Usage
renderWhenVisible(document.querySelector('#chart-region'), regionOptions)

Shared tooltip on time-series charts

For a main chart with multiple series, tooltip.shared: true renders one tooltip for all series at the hovered x position instead of a separate tooltip per series. This reduces tooltip DOM mutations on every mousemove.

tooltip: {
  shared: true,
  intersect: false
}

Setting intersect: false alongside shared: true ensures the tooltip appears whenever the cursor is near any x position, not only when it is directly over a data point.