Responsive Data Visualization React Mobile
Charts on mobile need different treatment than desktop: smaller screens demand larger fonts, fewer data points, and touch-friendly interactions (no hover, tap to select). This article teaches you to build responsive charts that look great at any screen size, from watches (320px) to desktops (2560px+). You'll learn container queries, media breakpoints, touch detection, and when to hide or simplify chart elements on small screens.
The Challenge: Charts Across All Screens
A line chart with 100 points and tiny axis labels is unreadable on a 375px phone but perfect on a 1920px monitor. Responsive charting isn't just scaling; it's redesigning the chart for each context. Desktop users hover to explore; mobile users tap. Desktop screens show legends; mobile screens need collapsible overlays. Responsive charts require thinking about the full experience.
In 2026, mobile web traffic represents 62% of overall web usage globally. Charts that don't work on mobile lose half your audience. Building responsive-first is essential.
Using ResponsiveContainer (Recharts)
Recharts provides ResponsiveContainer, which wraps a chart and scales it to fill its parent:
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer
} from "recharts";
export function ResponsiveLineChart({ data }) {
return (
<div style={{ width: "100%", height: "400px" }}>
<ResponsiveContainer>
<LineChart data={data} margin={{ top: 5, right: 30, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="revenue" stroke="#8884d8" />
</LineChart>
</ResponsiveContainer>
</div>
);
}
The parent div constrains size; ResponsiveContainer fills it. The chart scales to fit. This works for all Recharts chart types.
Custom Responsive SVG Charts with viewBox
For custom SVG charts (not using Recharts), use viewBox to scale content to the viewport:
import React, { useState, useEffect } from "react";
import { scaleLinear, scaleBand } from "d3-scale";
export function ResponsiveBarChart({ data }) {
const [dimensions, setDimensions] = useState({
width: 600,
height: 400
});
useEffect(() => {
const handleResize = () => {
const container = document.getElementById("chart-container");
if (container) {
setDimensions({
width: container.clientWidth,
height: Math.max(300, container.clientWidth * 0.6) // maintain aspect ratio
});
}
};
window.addEventListener("resize", handleResize);
handleResize();
return () => window.removeEventListener("resize", handleResize);
}, []);
const margin = { top: 20, right: 20, bottom: 40, left: 50 };
const innerWidth = Math.max(100, dimensions.width - margin.left - margin.right);
const innerHeight = Math.max(100, dimensions.height - margin.top - margin.bottom);
const xScale = scaleBand()
.domain(data.map((d) => d.label))
.range([0, innerWidth])
.padding(0.1);
const yScale = scaleLinear()
.domain([0, Math.max(...data.map((d) => d.value))])
.range([innerHeight, 0]);
return (
<div
id="chart-container"
style={{ width: "100%", height: "400px" }}
>
<svg
viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}
preserveAspectRatio="xMidYMid meet"
style={{ width: "100%", height: "100%" }}
>
<g transform={`translate(${margin.left}, ${margin.top})`}>
{data.map((d, i) => (
<rect
key={i}
x={xScale(d.label)}
y={yScale(d.value)}
width={xScale.bandwidth()}
height={innerHeight - yScale(d.value)}
fill="steelblue"
/>
))}
</g>
{/* Axes */}
<g transform={`translate(${margin.left}, ${dimensions.height - margin.bottom})`}>
{xScale.domain().map((label, i) => (
<g key={i} transform={`translate(${xScale(label) + xScale.bandwidth() / 2}, 0)`}>
<line y1="0" y2="5" stroke="black" />
<text
y="20"
textAnchor="middle"
fontSize={innerWidth < 300 ? "10" : "12"}
>
{label}
</text>
</g>
))}
</g>
</svg>
</div>
);
}
The viewBox attribute tells the SVG to scale its internal coordinate system. preserveAspectRatio="xMidYMid meet" centers and maintains aspect ratio. The chart recomputes scales on resize, ensuring it always fills the container.
Media Queries for Conditional Rendering
Different screen sizes call for different visualizations:
import React, { useState, useEffect } from "react";
export function AdaptiveChart({ data }) {
const [screenSize, setScreenSize] = useState("desktop");
useEffect(() => {
const updateScreenSize = () => {
if (window.innerWidth < 480) setScreenSize("mobile");
else if (window.innerWidth < 768) setScreenSize("tablet");
else setScreenSize("desktop");
};
window.addEventListener("resize", updateScreenSize);
updateScreenSize();
return () => window.removeEventListener("resize", updateScreenSize);
}, []);
if (screenSize === "mobile") {
// Simplified chart for mobile: fewer data points, larger fonts
return (
<div>
<h3>Top Performers</h3>
{data.slice(0, 3).map((d, i) => (
<div
key={i}
style={{
padding: "10px",
borderBottom: "1px solid #ddd",
fontSize: "16px"
}}
>
<strong>{d.label}</strong>: {d.value}
</div>
))}
</div>
);
}
if (screenSize === "tablet") {
// Medium detail: bar chart with larger bars and labels
return (
<div style={{ fontSize: "14px" }}>
{/* Bar chart with adjusted sizing */}
</div>
);
}
// Desktop: full chart with all details
return (
<div>
{/* Complex chart with legend, multiple series, etc. */}
</div>
);
}
This renders different UI based on screen size. Mobile gets a simple list; tablet a smaller chart; desktop the full visualization. This is a mobile-first approach: start with simplicity, add complexity as space allows.
Touch Interaction: Tap Instead of Hover
Mobile has no hover; users tap. Update your interaction model:
import React, { useState } from "react";
export function TouchFriendlyChart({ data }) {
const [selectedIndex, setSelectedIndex] = useState(null);
const handleBarPress = (index) => {
// On mobile, toggle selection on tap
setSelectedIndex(selectedIndex === index ? null : index);
};
const maxValue = Math.max(...data.map((d) => d.value));
return (
<div style={{ padding: "20px" }}>
<div style={{ display: "flex", gap: "10px", alignItems: "flex-end" }}>
{data.map((d, i) => (
<div
key={i}
onClick={() => handleBarPress(i)}
style={{
flex: 1,
display: "flex",
flexDirection: "column",
alignItems: "center",
cursor: "pointer",
minWidth: "0"
}}
>
<div
style={{
width: "100%",
height: `${(d.value / maxValue) * 150}px`,
backgroundColor:
selectedIndex === i ? "orange" : "steelblue",
borderRadius: "4px",
transition: "background-color 0.2s"
}}
/>
<label
style={{
marginTop: "8px",
fontSize: "12px",
wordBreak: "break-word",
textAlign: "center"
}}
>
{d.label}
</label>
{/* Show details below bar on tap */}
{selectedIndex === i && (
<div
style={{
marginTop: "8px",
padding: "8px",
backgroundColor: "#f0f0f0",
borderRadius: "4px",
fontSize: "14px",
fontWeight: "bold",
whiteSpace: "nowrap"
}}
>
{d.value}
</div>
)}
</div>
))}
</div>
</div>
);
}
Tapping a bar toggles selection and shows details. This is better than hover for mobile: users see what they tapped, and details persist for reading.
Simplifying Charts for Small Screens
On mobile, hide or simplify complex elements:
const Mobile = ({ data, isMobile }) => {
return (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="month"
tick={{ fontSize: isMobile ? 10 : 12 }}
angle={isMobile ? -45 : 0}
height={isMobile ? 60 : 30}
/>
<YAxis
tick={{ fontSize: isMobile ? 10 : 12 }}
width={isMobile ? 40 : 60}
/>
{/* Hide legend on mobile to save space */}
{!isMobile && <Legend />}
{/* Simpler tooltip on mobile */}
<Tooltip
contentStyle={{
fontSize: isMobile ? "12px" : "14px",
padding: isMobile ? "4px 8px" : "8px 12px"
}}
/>
{/* Show fewer lines on mobile */}
<Line dataKey="sales" stroke="#8884d8" />
{!isMobile && <Line dataKey="profit" stroke="#82ca9d" />}
</LineChart>
</ResponsiveContainer>
);
};
Adjust font sizes, hide redundant elements (legends, second series), and change angles for better readability on small screens.
Touch-Friendly Hit Targets
Make interactive elements large enough to tap accurately (minimum 44x44px):
export function MobileOptimizedChart({ data }) {
// Each bar is at least 44px wide
const minBarWidth = 44;
const containerWidth = Math.max(data.length * minBarWidth, 300);
return (
<div
style={{
overflowX: "auto",
width: "100%"
}}
>
<div style={{ width: `${containerWidth}px`, padding: "20px" }}>
{/* Chart with adequate touch targets */}
</div>
</div>
);
}
If bars are too small, allow horizontal scrolling instead of cramping them into the viewport.
Testing Responsiveness
Use Chrome DevTools or similar:
- Open DevTools (
F12) - Toggle Device Toolbar (
Ctrl+Shift+M) - Test at multiple breakpoints: 375px (iPhone SE), 768px (iPad), 1440px (desktop)
Measure:
- Readability: Can you read all labels?
- Interactivity: Can you tap/click accurately?
- Performance: Does the chart render smoothly on throttled mobile networks?
Key Takeaways
- Responsive charts adapt to screen size, not just scale proportionally.
- Use
ResponsiveContainer(Recharts) orviewBox(custom SVG) for fluid layouts. - Mobile charts need tap interaction, larger fonts, and simplified elements.
- Test at multiple breakpoints: mobile (375px), tablet (768px), desktop (1440px+).
- Implement media queries or size detection to conditionally render different visualizations per context.
Frequently Asked Questions
How do I detect if a user is on mobile?
Use media queries in CSS or check window.innerWidth in JavaScript. For React, check at mount and on resize:
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth < 768);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
Should I show fewer data points on mobile?
Yes, sample or aggregate data. Mobile screens can't show 100 bars clearly; sample every Nth point or group by day instead of hour.
How do I optimize chart performance on low-end mobile devices?
Use Canvas instead of SVG for large datasets; reduce animation complexity; disable features like tooltips on hover (not available on touch anyway); defer non-critical rendering with requestIdleCallback.
Can I use CSS media queries with SVG charts?
Partially. You can wrap the SVG in a media-query-responsive div, but SVG internals (text size, spacing) don't respond to media queries. Use JavaScript to adjust scales and re-render.
What about landscape vs portrait orientation?
Listen to the orientationchange event or re-measure on resize. Most charts handle both if you use responsive scaling. Test both orientations.