Adding Interactive Charts to Your Static Astro Site with Recharts
Learn how to add interactive React charts to Astro using Islands architecture. Get the interactivity you need without shipping JavaScript you don't.
187KB of JavaScript. 1.8 seconds to interactive. Lighthouse score: 74.
That was my dashboard page. Three paragraphs of text, a header, and one chart users could hover over. React hydrated all of it - every static paragraph, every unchanging heading - just so one component could respond to mouse events.
Here’s the thing: what if you could keep writing React components but only ship JavaScript for the parts that actually need it? That’s what Astro’s Islands architecture gives you. Same chart, same hover interactions, but now Lighthouse scores 98 because the static content stays static.
What Hydration Actually Means
When a server renders HTML, it’s just static markup. “Hydration” is the process of attaching JavaScript event handlers to make it interactive. In traditional SPAs, hydration happens to everything on the page, even content that doesn’t need it.
Astro flips this. By default, nothing gets hydrated. Your page is pure HTML and CSS - fast, lightweight, works without JavaScript. When you need interactivity, you opt in with a client: directive, creating an “island” of JavaScript in a sea of static content.
Think of it like this: instead of flooding the entire page with JavaScript and hoping it drains quickly, you’re placing specific puddles exactly where you need them.
Why Recharts
For React-based charting in Astro, Recharts hits the sweet spot:
- Declarative API - Compose charts from React components you already understand
- Reasonable size - ~40KB gzipped for basic charts (vs 60KB+ for some alternatives)
- Good defaults - Tooltips, legends, and responsive sizing work out of the box
- Active maintenance - Regular updates, good TypeScript support
It’s not the smallest option (Chart.js is lighter), but if you’re already in the React ecosystem, Recharts feels natural.
Step-by-Step Implementation
1. Add React Support to Astro
npx astro add react
This installs @astrojs/react and updates your config. Say yes to everything.
2. Install Recharts
npm install recharts
3. Create Your Chart Component
Create src/components/SalesChart.tsx:
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts';
const data = [
{ month: 'Jan', sales: 4000 },
{ month: 'Feb', sales: 3000 },
{ month: 'Mar', sales: 5000 },
{ month: 'Apr', sales: 4500 },
{ month: 'May', sales: 6000 },
{ month: 'Jun', sales: 5500 },
];
export default function SalesChart() {
return (
<div style={{ width: '100%', height: 300 }}>
<ResponsiveContainer>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Line
type="monotone"
dataKey="sales"
stroke="#8884d8"
strokeWidth={2}
/>
</LineChart>
</ResponsiveContainer>
</div>
);
}
4. Use It in an Astro Page
Create or update src/pages/dashboard.astro:
---
import BaseLayout from '../layouts/BaseLayout.astro';
import SalesChart from '../components/SalesChart';
---
<BaseLayout title="Dashboard">
<h1>Monthly Sales</h1>
<p>Here's how we performed this quarter. Hover over the chart for details.</p>
<SalesChart client:visible />
<p>Sales increased 37% compared to last year.</p>
</BaseLayout>
That client:visible is the magic. The chart only loads JavaScript when it scrolls into view.
Live Demo
Here’s the exact technique in action. This chart is rendered on this page using client:visible - it only loaded JavaScript when you scrolled down to it. Hover over the data points to see the tooltip:
Notice how the rest of this page loaded instantly? That’s because the chart’s JavaScript didn’t block anything. The text you’re reading right now was pure HTML from the start.
Client Directives: When to Use Each
Astro gives you five ways to control hydration:
| Directive | When It Hydrates | Best For |
|---|---|---|
client:load | Immediately on page load | Above-fold interactive elements |
client:idle | After page becomes idle | Important but not urgent interactivity |
client:visible | When scrolled into view | Charts, maps, anything below the fold |
client:media="(max-width: 768px)" | When media query matches | Mobile-only interactions |
client:only="react" | Never server-renders | Components that can’t SSR (window-dependent) |
For charts, I almost always use client:visible. Most dashboards have charts below the initial viewport, and there’s no reason to load that JavaScript until the user scrolls down.
If your chart is the first thing users see, use client:idle - it waits for the browser to finish critical work before hydrating.
The Performance Difference
I built a simple dashboard with a header, two paragraphs, and one Recharts line chart. Here’s what I measured:
Full React SPA (Create React App)
- Total JS: 187KB
- Time to Interactive: 1.8s
- Lighthouse Performance: 74
Astro with client:load
- Total JS: 89KB (React + Recharts only)
- Time to Interactive: 0.9s
- Lighthouse Performance: 91
Astro with client:visible
- Total JS: 89KB (but deferred)
- Time to Interactive: 0.4s
- Lighthouse Performance: 98
Same chart. Same interactivity. The client:visible version scores 24 points higher on Lighthouse because JavaScript doesn’t block initial render.
The key insight: you’re not shipping less JavaScript total - you’re shipping it smarter. The chart JS still arrives, but it doesn’t compete with rendering the content users actually see first.
Passing Dynamic Data
Real charts need real data. Here’s how to pass it from Astro to your React component:
---
// Fetch data at build time or request time
const response = await fetch('https://api.example.com/sales');
const salesData = await response.json();
---
<SalesChart client:visible data={salesData} />
Update your component to accept props:
interface SalesData {
month: string;
sales: number;
}
interface Props {
data: SalesData[];
}
export default function SalesChart({ data }: Props) {
return (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>{/* ... same as before */}</LineChart>
</ResponsiveContainer>
);
}
The data serializes to HTML as a prop, then hydrates with the component. No extra fetch needed on the client.
When This Approach Doesn’t Work
Islands architecture assumes most of your page is static. If you’re building something where everything is interactive - a real-time trading dashboard, a collaborative editor - you probably want a full SPA.
But for content sites with sprinkles of interactivity? Marketing pages with one animated chart? Documentation with interactive code playgrounds? Astro with targeted hydration is hard to beat.
Getting Started
The full setup takes about five minutes:
npm create astro@latest my-dashboard
cd my-dashboard
npx astro add react
npm install recharts
Then drop in your chart component with client:visible and watch your Lighthouse scores climb.
The mental shift from “hydrate everything” to “hydrate only what needs it” takes some getting used to. But once you internalize it, you’ll find yourself questioning why we ever shipped so much JavaScript in the first place.