Building zero-JS charts with SVG and Astro
How we replaced a 200 kB charting library with build-time SVG, without losing tooltips or accessibility.
1 GitHub star = 1 new component.
Star the repoโLatest articles with a featured post and a grid of cards, gradient covers, categories and read time.
Engineeringยท8 min read
How we replaced a 200 kB charting library with build-time SVG, without losing tooltips or accessibility.
By Marcus Lee
Designยท5 min read
Contrast, elevation and color tokens that hold up at night.
By Sofรญa Ruiz
Accessibilityยท6 min read
Focus, keyboard, contrast and screen readers โ our exact process.
By Priya Natarajan
Growthยท7 min read
A data-backed look at hero, proof, pricing and FAQ placement.
By Alicia Gรณmez
Copy and paste this code into your React component:
import { BlogGridSection } from 'react-component-kit-ui/sections';
<BlogGridSection posts={[
{ title: 'Zero-JS charts', excerpt: 'โฆ', category: 'Engineering', date: '2026-10-28', readTime: '8 min', author: 'Marcus', href: '/blog/charts' },
]} />If you are not using the npm package, create a new file and paste the following code: src/components/sections/BlogGridSection.tsx + BlogGridSection.css:
'use client';
import type { CSSProperties } from 'react';
import './BlogGridSection.css';
/** class:list โ className */
function cx(...args: unknown[]): string {
const out: string[] = [];
const walk = (v: unknown) => {
if (!v) return;
if (typeof v === 'string' || typeof v === 'number') out.push(String(v));
else if (Array.isArray(v)) v.forEach(walk);
else if (typeof v === 'object') for (const [k, on] of Object.entries(v as Record<string, unknown>)) if (on) out.push(k);
};
args.forEach(walk);
return out.join(' ');
}
/** "a: b; --c: d" โ style object */
function st(css: string | undefined): CSSProperties {
const o: Record<string, string> = {};
for (const decl of String(css ?? '').split(';')) {
const i = decl.indexOf(':');
if (i < 0) continue;
const k = decl.slice(0, i).trim();
if (k) o[k.startsWith('--') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = decl.slice(i + 1).trim();
}
return o as CSSProperties;
}
interface Post { title: string; excerpt: string; category: string; date: string; readTime: string; author: string; href: string; image?: string }
/**
* BlogGridSection โ Latest articles section with one featured post and a grid of
* smaller posts. Covers are CSS gradients by default (pass `image` for photos).
*
* @param {string} title - Section heading.
* @param {{label: string, href: string}} viewAll - Link to the full blog.
* @param {Array<{title: string, excerpt: string, category: string, date: string, readTime: string, author: string, href: string, image?: string}>} posts - Posts (first is featured).
* @param {string} variant - Card style (cards|minimal|overlay).
* @param {string} size - Vertical rhythm (sm|md|lg).
* @param {string} className - Extra CSS class.
*/
export interface BlogGridSectionProps {
title?: string;
viewAll?: { label: string; href: string };
posts?: Post[];
variant?: 'cards' | 'minimal' | 'overlay';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
export function BlogGridSection({
title = 'From the blog',
viewAll = { label: 'View all posts', href: '#' },
posts = [
{ title: 'Building zero-JS charts with SVG and Astro', excerpt: 'How we replaced a 200 kB charting library with build-time SVG, without losing tooltips or accessibility.', category: 'Engineering', date: '2026-10-28', readTime: '8 min', author: 'Marcus Lee', href: '#' },
{ title: 'Designing for dark mode first', excerpt: 'Contrast, elevation and color tokens that hold up at night.', category: 'Design', date: '2026-10-21', readTime: '5 min', author: 'Sofรญa Ruiz', href: '#' },
{ title: 'The accessibility checklist we run on every component', excerpt: 'Focus, keyboard, contrast and screen readers โ our exact process.', category: 'Accessibility', date: '2026-10-14', readTime: '6 min', author: 'Priya Natarajan', href: '#' },
{ title: 'Landing page anatomy: 7 sections that convert', excerpt: 'A data-backed look at hero, proof, pricing and FAQ placement.', category: 'Growth', date: '2026-10-07', readTime: '7 min', author: 'Alicia Gรณmez', href: '#' },
],
variant = 'cards',
size = 'md',
className = '',
}: BlogGridSectionProps) {
const id = `bgs-${Math.random().toString(36).slice(2, 8)}`;
const gradients = [
'radial-gradient(circle at 20% 20%, rgba(57,135,229,.65), transparent 55%), radial-gradient(circle at 80% 80%, rgba(144,133,233,.55), transparent 50%), #101018',
'radial-gradient(circle at 70% 30%, color-mix(in srgb, var(--c-primary-light, #d4a017) 60%, transparent), transparent 50%), #15120a',
'radial-gradient(circle at 30% 70%, rgba(25,158,112,.6), transparent 55%), #0b1411',
'radial-gradient(circle at 60% 40%, rgba(213,81,129,.55), transparent 55%), #150b10',
];
const fmtDate = (d: string) => new Date(`${d}T00:00:00Z`).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' });
return (
<>
<div className="cq-blog-grid-section-cq" data-rk-1eb7bb="">
<section className={`bgs bgs--${variant} bgs--${size} ${className}`} aria-labelledby={id} data-rk-1eb7bb="">
<div className="bgs__inner" data-rk-1eb7bb="">
<header className="bgs__head" data-rk-1eb7bb="">
<h2 id={id} className="bgs__title" data-rk-1eb7bb="">{title}</h2>
{viewAll && (
<a className="bgs__all" href={viewAll.href} data-rk-1eb7bb="">
{viewAll.label}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true" data-rk-1eb7bb=""><path d="M5 12h14M13 6l6 6-6 6" data-rk-1eb7bb="" /></svg>
</a>
)}
</header>
<ul className="bgs__grid" data-rk-1eb7bb="">
{posts.map((p, i) => (
<li key={i} className={cx(['bgs__post', { 'bgs__post--featured': i === 0 }])} data-rk-1eb7bb="">
<article className="bgs__card" data-rk-1eb7bb="">
<div className="bgs__cover" style={st(p.image ? undefined : `background:${gradients[i % gradients.length]}`)} data-rk-1eb7bb="">
{p.image && <img src={p.image} alt="" loading="lazy" decoding="async" data-rk-1eb7bb="" />}
</div>
<div className="bgs__body" data-rk-1eb7bb="">
<p className="bgs__meta" data-rk-1eb7bb="">
<span className="bgs__cat" data-rk-1eb7bb="">{p.category}</span>
<time dateTime={p.date} data-rk-1eb7bb="">{fmtDate(p.date)}</time>
<span aria-hidden="true" data-rk-1eb7bb="">ยท</span>
<span data-rk-1eb7bb="">{p.readTime} read</span>
</p>
<h3 className="bgs__post-title" data-rk-1eb7bb=""><a href={p.href} data-rk-1eb7bb="">{p.title}</a></h3>
<p className="bgs__excerpt" data-rk-1eb7bb="">{p.excerpt}</p>
<p className="bgs__author" data-rk-1eb7bb="">By {p.author}</p>
</div>
</article>
</li>
))}
</ul>
</div>
</section>
</div>
</>
);
}
export default BlogGridSection;/* BlogGridSection โ ported from astro-component-kit (sections/BlogGridSection.astro) */
.cq-blog-grid-section-cq:where([data-rk-1eb7bb]) { container-type: inline-size; }
.bgs:where([data-rk-1eb7bb]) {
--bgs-py: 5.5rem;
padding: var(--bgs-py) var(--sp-6, 1.5rem);
background: var(--c-bg, #050505);
}
.bgs--sm:where([data-rk-1eb7bb]) { --bgs-py: 3.5rem; }
.bgs--lg:where([data-rk-1eb7bb]) { --bgs-py: 7.5rem; }
.bgs__inner:where([data-rk-1eb7bb]) { max-width: 1180px; margin: 0 auto; }
.bgs__head:where([data-rk-1eb7bb]) { display: flex; justify-content: space-between; align-items: flex-end; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap; }
.bgs__title:where([data-rk-1eb7bb]) { margin: 0; font-size: clamp(1.75rem, 4vw, 2.5rem); font-weight: 800; letter-spacing: -0.03em; color: var(--c-text-1, #fff); }
.bgs__all:where([data-rk-1eb7bb]) { display: inline-flex; align-items: center; gap: 0.4rem; min-height: 44px; font-weight: 600; color: var(--c-primary-light, #d4a017); text-decoration: none; }
.bgs__all:focus-visible:where([data-rk-1eb7bb]) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; border-radius: 4px; }
.bgs__grid:where([data-rk-1eb7bb]) { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.25rem; }
.bgs__post--featured:where([data-rk-1eb7bb]) { grid-column: 1 / -1; }
.bgs__card:where([data-rk-1eb7bb]) { position: relative; height: 100%; display: flex; flex-direction: column; border-radius: var(--r-lg, 20px); overflow: hidden; transition: transform 0.25s ease, border-color 0.25s ease; }
.bgs--cards:where([data-rk-1eb7bb]) .bgs__card:where([data-rk-1eb7bb]) { background: rgb(var(--ink-rgb, 255 255 255) / 0.025); border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.07)); }
.bgs__card:hover:where([data-rk-1eb7bb]), .bgs__card:focus-within:where([data-rk-1eb7bb]) { transform: translateY(-3px); border-color: rgb(var(--ink-rgb, 255 255 255) / 0.18); }
.bgs__post--featured:where([data-rk-1eb7bb]) .bgs__card:where([data-rk-1eb7bb]) { display: grid; grid-template-columns: 1.2fr 1fr; }
.bgs__cover:where([data-rk-1eb7bb]) { aspect-ratio: 16 / 9; position: relative; }
.bgs__post--featured:where([data-rk-1eb7bb]) .bgs__cover:where([data-rk-1eb7bb]) { aspect-ratio: auto; min-height: 280px; }
.bgs__cover:where([data-rk-1eb7bb]) img:where([data-rk-1eb7bb]) { width: 100%; height: 100%; object-fit: cover; display: block; }
.bgs--minimal:where([data-rk-1eb7bb]) .bgs__cover:where([data-rk-1eb7bb]) { border-radius: var(--r-lg, 20px); }
.bgs__body:where([data-rk-1eb7bb]) { padding: 1.5rem; display: flex; flex-direction: column; gap: 0.6rem; }
.bgs--minimal:where([data-rk-1eb7bb]) .bgs__body:where([data-rk-1eb7bb]) { padding: 1.25rem 0 0; }
.bgs__post--featured:where([data-rk-1eb7bb]) .bgs__body:where([data-rk-1eb7bb]) { justify-content: center; padding: clamp(1.5rem, 4vw, 2.75rem); }
.bgs--overlay:where([data-rk-1eb7bb]) .bgs__card:where([data-rk-1eb7bb]) { min-height: 320px; }
.bgs--overlay:where([data-rk-1eb7bb]) .bgs__cover:where([data-rk-1eb7bb]) { position: absolute; inset: 0; aspect-ratio: auto; }
.bgs--overlay:where([data-rk-1eb7bb]) .bgs__cover:where([data-rk-1eb7bb])::after { content: ''; position: absolute; inset: 0; background: linear-gradient(to top, rgba(0, 0, 0, 0.85), transparent 70%); }
.bgs--overlay:where([data-rk-1eb7bb]) .bgs__body:where([data-rk-1eb7bb]) { position: relative; margin-top: auto; }
.bgs--overlay:where([data-rk-1eb7bb]) .bgs__post--featured:where([data-rk-1eb7bb]) .bgs__card:where([data-rk-1eb7bb]) { display: flex; }
.bgs__meta:where([data-rk-1eb7bb]) { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0; font-size: 0.8125rem; color: var(--c-text-2, #a3a3a3); }
.bgs__cat:where([data-rk-1eb7bb]) { padding: 0.15rem 0.55rem; border-radius: 999px; background: rgb(var(--ink-rgb, 255 255 255) / 0.07); color: var(--c-text-1, #fff); font-weight: 600; font-size: 0.75rem; }
.bgs__post-title:where([data-rk-1eb7bb]) { margin: 0; font-size: 1.1875rem; font-weight: 700; line-height: 1.3; letter-spacing: -0.015em; }
.bgs__post--featured:where([data-rk-1eb7bb]) .bgs__post-title:where([data-rk-1eb7bb]) { font-size: clamp(1.5rem, 3vw, 2rem); }
.bgs__post-title:where([data-rk-1eb7bb]) a:where([data-rk-1eb7bb]) { color: var(--c-text-1, #fff); text-decoration: none; }
.bgs__post-title:where([data-rk-1eb7bb]) a:where([data-rk-1eb7bb])::after { content: ''; position: absolute; inset: 0; }
.bgs__post-title:where([data-rk-1eb7bb]) a:focus-visible:where([data-rk-1eb7bb]) { outline: none; }
.bgs__card:has(a:focus-visible):where([data-rk-1eb7bb]) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; }
.bgs__excerpt:where([data-rk-1eb7bb]) { margin: 0; font-size: 0.9375rem; line-height: 1.6; color: var(--c-text-2, #a3a3a3); }
.bgs__author:where([data-rk-1eb7bb]) { margin: auto 0 0; padding-top: 0.5rem; font-size: 0.8125rem; color: var(--c-text-3, #8a8a8a); }
@container (max-width: 900px) {
.bgs__grid:where([data-rk-1eb7bb]) { grid-template-columns: 1fr 1fr; }
.bgs__post--featured:where([data-rk-1eb7bb]) .bgs__card:where([data-rk-1eb7bb]) { grid-template-columns: 1fr; }
.bgs__post--featured:where([data-rk-1eb7bb]) .bgs__cover:where([data-rk-1eb7bb]) { min-height: 200px; }
}
@container (max-width: 600px) { .bgs__grid:where([data-rk-1eb7bb]) { grid-template-columns: 1fr; } }
@media (prefers-reduced-motion: reduce) { .bgs__card:where([data-rk-1eb7bb]) { transition: none; } }