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 Astro page:

astro
---
import { BlogGridSection } from 'astro-component-kit/sections';
---

<BlogGridSection
  posts={[
    { title: 'Zero-JS charts', excerpt: '…', category: 'Engineering', date: '2026-10-28', readTime: '8 min', author: 'Marcus', href: '/blog/charts' },
  ]}
/>
--- import { BlogGridSection } from 'astro-component-kit/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.astro:

astro
---
/**
 * 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.
 */
interface Post { title: string; excerpt: string; category: string; date: string; readTime: string; author: string; href: string; image?: string }
interface Props {
  title?: string;
  viewAll?: { label: string; href: string };
  posts?: Post[];
  variant?: 'cards' | 'minimal' | 'overlay';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

const {
  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 = '',
} = Astro.props;

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' });
---

<div class="cq-blog-grid-section-cq">
<section class={`bgs bgs--${variant} bgs--${size} ${className}`} aria-labelledby={id}>
  <div class="bgs__inner">
    <header class="bgs__head">
      <h2 id={id} class="bgs__title">{title}</h2>
      {viewAll && (
        <a class="bgs__all" href={viewAll.href}>
          {viewAll.label}
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" /></svg>
        </a>
      )}
    </header>
    <ul class="bgs__grid">
      {posts.map((p, i) => (
        <li class:list={['bgs__post', { 'bgs__post--featured': i === 0 }]}>
          <article class="bgs__card">
            <div class="bgs__cover" style={p.image ? undefined : `background:${gradients[i % gradients.length]}`}>
              {p.image && <img src={p.image} alt="" loading="lazy" decoding="async" />}
            </div>
            <div class="bgs__body">
              <p class="bgs__meta">
                <span class="bgs__cat">{p.category}</span>
                <time datetime={p.date}>{fmtDate(p.date)}</time>
                <span aria-hidden="true">·</span>
                <span>{p.readTime} read</span>
              </p>
              <h3 class="bgs__post-title"><a href={p.href}>{p.title}</a></h3>
              <p class="bgs__excerpt">{p.excerpt}</p>
              <p class="bgs__author">By {p.author}</p>
            </div>
          </article>
        </li>
      ))}
    </ul>
  </div>
</section>
</div>
<style>
  .cq-blog-grid-section-cq { container-type: inline-size; }
  .bgs {
    --bgs-py: 5.5rem;
    padding: var(--bgs-py) var(--sp-6, 1.5rem);
    background: var(--c-bg, #050505);
  }
  .bgs--sm { --bgs-py: 3.5rem; }
  .bgs--lg { --bgs-py: 7.5rem; }
  .bgs__inner { max-width: 1180px; margin: 0 auto; }
  .bgs__head { display: flex; justify-content: space-between; align-items: flex-end; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap; }
  .bgs__title { 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 { 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 { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; border-radius: 4px; }
  .bgs__grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.25rem; }
  .bgs__post--featured { grid-column: 1 / -1; }
  .bgs__card { 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 .bgs__card { 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, .bgs__card:focus-within { transform: translateY(-3px); border-color: rgb(var(--ink-rgb, 255 255 255) / 0.18); }
  .bgs__post--featured .bgs__card { display: grid; grid-template-columns: 1.2fr 1fr; }
  .bgs__cover { aspect-ratio: 16 / 9; position: relative; }
  .bgs__post--featured .bgs__cover { aspect-ratio: auto; min-height: 280px; }
  .bgs__cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .bgs--minimal .bgs__cover { border-radius: var(--r-lg, 20px); }
  .bgs__body { padding: 1.5rem; display: flex; flex-direction: column; gap: 0.6rem; }
  .bgs--minimal .bgs__body { padding: 1.25rem 0 0; }
  .bgs__post--featured .bgs__body { justify-content: center; padding: clamp(1.5rem, 4vw, 2.75rem); }
  .bgs--overlay .bgs__card { min-height: 320px; }
  .bgs--overlay .bgs__cover { position: absolute; inset: 0; aspect-ratio: auto; }
  .bgs--overlay .bgs__cover::after { content: ''; position: absolute; inset: 0; background: linear-gradient(to top, rgba(0, 0, 0, 0.85), transparent 70%); }
  .bgs--overlay .bgs__body { position: relative; margin-top: auto; }
  .bgs--overlay .bgs__post--featured .bgs__card { display: flex; }
  .bgs__meta { 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 { 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 { margin: 0; font-size: 1.1875rem; font-weight: 700; line-height: 1.3; letter-spacing: -0.015em; }
  .bgs__post--featured .bgs__post-title { font-size: clamp(1.5rem, 3vw, 2rem); }
  .bgs__post-title a { color: var(--c-text-1, #fff); text-decoration: none; }
  .bgs__post-title a::after { content: ''; position: absolute; inset: 0; }
  .bgs__post-title a:focus-visible { outline: none; }
  .bgs__card:has(a:focus-visible) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; }
  .bgs__excerpt { margin: 0; font-size: 0.9375rem; line-height: 1.6; color: var(--c-text-2, #a3a3a3); }
  .bgs__author { margin: auto 0 0; padding-top: 0.5rem; font-size: 0.8125rem; color: var(--c-text-3, #8a8a8a); }
  @container (max-width: 900px) {
    .bgs__grid { grid-template-columns: 1fr 1fr; }
    .bgs__post--featured .bgs__card { grid-template-columns: 1fr; }
    .bgs__post--featured .bgs__cover { min-height: 200px; }
  }
  @container (max-width: 600px) { .bgs__grid { grid-template-columns: 1fr; } }
  @media (prefers-reduced-motion: reduce) { .bgs__card { transition: none; } }
</style>
--- /** * 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. */ interface Post { title: string; excerpt: string; category: string; date: string; readTime: string; author: string; href: string; image?: string } interface Props { title?: string; viewAll?: { label: string; href: string }; posts?: Post[]; variant?: 'cards' | 'minimal' | 'overlay'; size?: 'sm' | 'md' | 'lg'; className?: string; } const { 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 = '', } = Astro.props; 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' }); --- <div class="cq-blog-grid-section-cq"> <section class={`bgs bgs--${variant} bgs--${size} ${className}`} aria-labelledby={id}> <div class="bgs__inner"> <header class="bgs__head"> <h2 id={id} class="bgs__title">{title}</h2> {viewAll && ( <a class="bgs__all" href={viewAll.href}> {viewAll.label} <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" /></svg> </a> )} </header> <ul class="bgs__grid"> {posts.map((p, i) => ( <li class:list={['bgs__post', { 'bgs__post--featured': i === 0 }]}> <article class="bgs__card"> <div class="bgs__cover" style={p.image ? undefined : `background:${gradients[i % gradients.length]}`}> {p.image && <img src={p.image} alt="" loading="lazy" decoding="async" />} </div> <div class="bgs__body"> <p class="bgs__meta"> <span class="bgs__cat">{p.category}</span> <time datetime={p.date}>{fmtDate(p.date)}</time> <span aria-hidden="true">·</span> <span>{p.readTime} read</span> </p> <h3 class="bgs__post-title"><a href={p.href}>{p.title}</a></h3> <p class="bgs__excerpt">{p.excerpt}</p> <p class="bgs__author">By {p.author}</p> </div> </article> </li> ))} </ul> </div> </section> </div> <style> .cq-blog-grid-section-cq { container-type: inline-size; } .bgs { --bgs-py: 5.5rem; padding: var(--bgs-py) var(--sp-6, 1.5rem); background: var(--c-bg, #050505); } .bgs--sm { --bgs-py: 3.5rem; } .bgs--lg { --bgs-py: 7.5rem; } .bgs__inner { max-width: 1180px; margin: 0 auto; } .bgs__head { display: flex; justify-content: space-between; align-items: flex-end; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap; } .bgs__title { 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 { 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 { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; border-radius: 4px; } .bgs__grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.25rem; } .bgs__post--featured { grid-column: 1 / -1; } .bgs__card { 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 .bgs__card { 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, .bgs__card:focus-within { transform: translateY(-3px); border-color: rgb(var(--ink-rgb, 255 255 255) / 0.18); } .bgs__post--featured .bgs__card { display: grid; grid-template-columns: 1.2fr 1fr; } .bgs__cover { aspect-ratio: 16 / 9; position: relative; } .bgs__post--featured .bgs__cover { aspect-ratio: auto; min-height: 280px; } .bgs__cover img { width: 100%; height: 100%; object-fit: cover; display: block; } .bgs--minimal .bgs__cover { border-radius: var(--r-lg, 20px); } .bgs__body { padding: 1.5rem; display: flex; flex-direction: column; gap: 0.6rem; } .bgs--minimal .bgs__body { padding: 1.25rem 0 0; } .bgs__post--featured .bgs__body { justify-content: center; padding: clamp(1.5rem, 4vw, 2.75rem); } .bgs--overlay .bgs__card { min-height: 320px; } .bgs--overlay .bgs__cover { position: absolute; inset: 0; aspect-ratio: auto; } .bgs--overlay .bgs__cover::after { content: ''; position: absolute; inset: 0; background: linear-gradient(to top, rgba(0, 0, 0, 0.85), transparent 70%); } .bgs--overlay .bgs__body { position: relative; margin-top: auto; } .bgs--overlay .bgs__post--featured .bgs__card { display: flex; } .bgs__meta { 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 { 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 { margin: 0; font-size: 1.1875rem; font-weight: 700; line-height: 1.3; letter-spacing: -0.015em; } .bgs__post--featured .bgs__post-title { font-size: clamp(1.5rem, 3vw, 2rem); } .bgs__post-title a { color: var(--c-text-1, #fff); text-decoration: none; } .bgs__post-title a::after { content: ''; position: absolute; inset: 0; } .bgs__post-title a:focus-visible { outline: none; } .bgs__card:has(a:focus-visible) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; } .bgs__excerpt { margin: 0; font-size: 0.9375rem; line-height: 1.6; color: var(--c-text-2, #a3a3a3); } .bgs__author { margin: auto 0 0; padding-top: 0.5rem; font-size: 0.8125rem; color: var(--c-text-3, #8a8a8a); } @container (max-width: 900px) { .bgs__grid { grid-template-columns: 1fr 1fr; } .bgs__post--featured .bgs__card { grid-template-columns: 1fr; } .bgs__post--featured .bgs__cover { min-height: 200px; } } @container (max-width: 600px) { .bgs__grid { grid-template-columns: 1fr; } } @media (prefers-reduced-motion: reduce) { .bgs__card { transition: none; } } </style>