1 GitHub star = 1 new component.

Star the repo
Blog & Content

Blog Grid Section

Latest articles with a featured post and a grid of cards, gradient covers, categories and read time.

blogarticlespostscontent
Live Preview

From the blog

View all posts

Usage

Copy and paste this code into your React component:

tsx
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' },
  ]} />

Manual Installation

If you are not using the npm package, create a new file and paste the following code: src/components/sections/BlogGridSection.tsx + BlogGridSection.css:

BlogGridSection.tsx
'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.css
/* 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; } }