roiui
itemcard-trafficsource: ok · hit
Traffic Card
A website traffic card component with line chart and statistics.
npx shadcn add "https://roiui.com/r/card-traffic.json"free
Where it goes
stats
Interaction
animated-counter
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumchartsrecharts
Registry dependencies
https://roiui.com/r/badge.jsonhttps://roiui.com/r/card.jsonhttps://roiui.com/r/chart.json
Files3 · source cached 10/14/2026, 2:04:50 PM
registry/brook/blocks/card-traffic/components/card-traffic.tsx
"use client";
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "../../ui/card/card";
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "../../ui/chart/chart";
import styles from "./card-traffic.module.css";
export type TrafficDataPoint = {
/** ISO date (YYYY-MM-DD), or a numeric day label. */
date: string | number;
desktop: number;
mobile: number;
};
export type CardTrafficProps = {
data: TrafficDataPoint[];
bounceRate?: number;
conversionRate?: number;
};
const formatDate = (value: unknown) => {
if (typeof value === "number") {
return `Day ${value}`;
}
const date = new Date(String(value));
return Number.isNaN(date.getTime())
? String(value)
: date.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
};
const chartConfig = {
mobile: {
label: "Mobile",
color: "var(--chart-1)",
},
desktop: {
label: "Desktop",
color: "var(--chart-2)",
},
} satisfies ChartConfig;
export function CardTraffic({ data, bounceRate, conversionRate }: CardTrafficProps) {
const mobile = data.reduce((sum, point) => sum + point.mobile, 0);
const desktop = data.reduce((sum, point) => sum + point.desktop, 0);
return (
<Card
className={styles.cardContainer}
style={{
maxWidth: "600px",
width: "100%",
}}
>
<CardHeader>
<CardTitle>Website Traffic</CardTitle>
</CardHeader>
<CardContent className={styles.chartContainer}>
<div style={{ width: "100%", height: "100%" }}>
<ChartContainer config={chartConfig}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="date" hide={true} tickFormatter={formatDate} />
<YAxis hide={true} />
<ChartTooltip content={<ChartTooltipContent labelFormatter={formatDate} />} />
<Line
animationDuration={800}
dataKey="mobile"
dot={false}
stroke="var(--color-mobile)"
strokeWidth={2}
type="monotone"
/>
<Line
animationBegin={200}
animationDuration={800}
dataKey="desktop"
dot={false}
stroke="var(--color-desktop)"
strokeWidth={2}
type="monotone"
/>
</LineChart>
</ChartContainer>
</div>
</CardContent>
<div className={styles.statsGrid}>
<div className={styles.statCard}>
<div className={styles.statHeader}>
<span className={styles.statLabel}>Mobile</span>
</div>
<span className={styles.statNumber}>{mobile.toLocaleString("en-US")}</span>
</div>
<div className={styles.statCard}>
<div className={styles.statHeader}>
<span className={styles.statLabel}>Desktop</span>
</div>
<span className={styles.statNumber}>{desktop.toLocaleString("en-US")}</span>
</div>
<div className={styles.statCard}>
<div className={styles.statHeader}>
<span className={styles.statLabel}>Bounce Rate</span>
</div>
<span className={styles.statNumber}>{bounceRate === undefined ? "Not provided" : `${bounceRate}%`}</span>
</div>
<div className={styles.statCard}>
<div className={styles.statHeader}>
<span className={styles.statLabel}>Conversion</span>
</div>
<span className={styles.statNumber}>
{conversionRate === undefined ? "Not provided" : `${conversionRate}%`}
</span>
</div>
</div>
</Card>
);
}
registry/brook/blocks/card-traffic/components/card-traffic.module.css
.cardContainer {
width: 100%;
max-width: 600px;
}
.chartContainer {
height: 200px;
padding: 0;
}
.statsGrid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
border-top: 0.5px solid oklch(from var(--border) l c h / 0.7);
margin: 0 -24px -24px -24px;
}
.statCard {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 8px;
border-right: 0.5px solid oklch(from var(--border) l c h / 0.7);
border-bottom: 0.5px solid oklch(from var(--border) l c h / 0.7);
}
.statCard:nth-child(2n) {
border-right: none;
}
.statCard:nth-child(n + 3) {
border-bottom: none;
}
@media (max-width: 640px) {
.cardContainer {
max-width: 100%;
}
.chartContainer {
height: 200px;
}
.statsGrid {
margin: 0 -20px -20px -20px;
}
.statCard {
padding: 1rem;
gap: 0.5rem;
}
.statLabel {
font-size: 0.875rem;
}
.statNumber {
font-size: 1.75rem;
font-weight: 700;
}
}
@media (max-width: 400px) {
.statsGrid {
grid-template-columns: 1fr;
grid-template-rows: auto;
margin: 0 -20px -20px -20px;
}
.statCard {
border-right: none;
border-bottom: 1px solid var(--border);
padding: 1rem;
}
.statCard:nth-child(2n) {
border-right: none;
}
.statCard:nth-child(n + 3) {
border-bottom: 1px solid var(--border);
}
.statCard:last-child {
border-bottom: none;
}
.statNumber {
font-size: 1.5rem;
}
}
.statHeader {
display: flex;
justify-content: space-between;
align-items: center;
}
.statLabel {
font-size: 14px;
line-height: 20px;
color: var(--muted-foreground);
font-weight: 500;
}
.statNumber {
font-size: 32px;
font-weight: 700;
color: var(--foreground);
line-height: 1;
font-variant-numeric: tabular-nums;
}
registry/brook/blocks/card-traffic/data.json
[
{
"date": "2025-07-14",
"desktop": 12.3,
"mobile": 28.7
},
{
"date": "2025-07-15",
"desktop": 11.8,
"mobile": 29.2
},
{
"date": "2025-07-16",
"desktop": 13.1,
"mobile": 31.4
},
{
"date": "2025-07-17",
"desktop": 12.9,
"mobile": 30.8
},
{
"date": "2025-07-18",
"desktop": 14.2,
"mobile": 33.1
},
{
"date": "2025-07-19",
"desktop": 13.7,
"mobile": 32.3
},
{
"date": "2025-07-20",
"desktop": 11.5,
"mobile": 27.9
},
{
"date": "2025-07-21",
"desktop": 10.8,
"mobile": 26.4
},
{
"date": "2025-07-22",
"desktop": 12.6,
"mobile": 29.8
},
{
"date": "2025-07-23",
"desktop": 13.9,
"mobile": 32.7
},
{
"date": "2025-07-24",
"desktop": 14.1,
"mobile": 33.9
},
{
"date": "2025-07-25",
"desktop": 13.3,
"mobile": 31.8
},
{
"date": "2025-07-26",
"desktop": 12.7,
"mobile": 30.2
},
{
"date": "2025-07-27",
"desktop": 11.9,
"mobile": 28.6
},
{
"date": "2025-07-28",
"desktop": 13.8,
"mobile": 32.4
},
{
"date": "2025-07-29",
"desktop": 14.5,
"mobile": 34.2
},
{
"date": "2025-07-30",
"desktop": 13.2,
"mobile": 31.1
},
{
"date": "2025-07-31",
"desktop": 12.4,
"mobile": 29.7
},
{
"date": "2025-08-01",
"desktop": 14.8,
"mobile": 35.1
},
{
"date": "2025-08-02",
"desktop": 15.2,
"mobile": 36.3
},
{
"date": "2025-08-03",
"desktop": 11.6,
"mobile": 28.2
},
{
"date": "2025-08-04",
"desktop": 10.9,
"mobile": 26.8
},
{
"date": "2025-08-05",
"desktop": 13.4,
"mobile": 32.1
},
{
"date": "2025-08-06",
"desktop": 14.7,
"mobile": 34.8
},
{
"date": "2025-08-07",
"desktop": 13.1,
"mobile": 31.7
},
{
"date": "2025-08-08",
"desktop": 12.8,
"mobile": 30.4
},
{
"date": "2025-08-09",
"desktop": 14.3,
"mobile": 33.6
},
{
"date": "2025-08-10",
"desktop": 15.1,
"mobile": 35.7
},
{
"date": "2025-08-11",
"desktop": 11.7,
"mobile": 28.9
},
{
"date": "2025-08-12",
"desktop": 12.2,
"mobile": 29.4
}
]