# Filterable Archive Index

> A dense blog archive whose subject pills are derived from the posts themselves and carry live counts, filtering a two-column list of hairline rows stamped with a date and a reading time.

A shadcn/ui block from the [@beste-ui](https://ui.beste.co) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/beste-ui/blog61).

- Page, with a live preview: https://shadcn.exchange/r/beste-ui/blog61
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/beste-ui/blog61-light-9677c053cd6a0366.png
- Registry item: https://ui.beste.co/r/blog61.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.beste.co/r/blog61.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `clsx`, `lucide-react`, `tailwind-merge`

## Files

### `registry/blog61/blog61.tsx`

```tsx
"use client";

import { Badge23 } from "@/components/beste/component/badge23";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { useState } from "react";

interface Badge {
  label: string;
}

interface ArchivePost {
  date: string;
  topic: string;
  title: string;
  readingTime: string;
  href: string;
}

interface Blog61Props {
  badge?: Badge;
  heading?: string;
  description?: string;
  allLabel?: string;
  posts?: ArchivePost[];
  emptyLabel?: string;
  className?: string;
}

export const blog61Demo: Blog61Props = {
  badge: { label: "Archive" },
  heading: "Everything we have written, by subject",
  description:
    "No infinite scroll and no recommended reading. Pick a subject and the list narrows to it.",
  allLabel: "Everything",
  posts: [
    {
      date: "12 May 2026",
      topic: "Operations",
      title: "The waiting list is a scheduling problem, not a queue",
      readingTime: "6 min",
      href: "https://beste.co",
    },
    {
      date: "28 Apr 2026",
      topic: "Billing",
      title: "Why we raise the invoice from the appointment, not the month",
      readingTime: "8 min",
      href: "https://beste.co",
    },
    {
      date: "16 Apr 2026",
      topic: "Migration",
      title: "What eleven years of records looks like when you export it",
      readingTime: "9 min",
      href: "https://beste.co",
    },
    {
      date: "09 Apr 2026",
      topic: "Security",
      title: "Six roles beat forty checkboxes, and we can prove it",
      readingTime: "5 min",
      href: "https://beste.co",
    },
    {
      date: "21 Mar 2026",
      topic: "Product",
      title: "We deleted the settings page and nobody noticed",
      readingTime: "4 min",
      href: "https://beste.co",
    },
    {
      date: "02 Mar 2026",
      topic: "Operations",
      title: "Twelve receptionists, one stopwatch, and a very long Tuesday",
      readingTime: "11 min",
      href: "https://beste.co",
    },
    {
      date: "18 Feb 2026",
      topic: "Billing",
      title: "Reconciliation is a reading task, not a data-entry one",
      readingTime: "7 min",
      href: "https://beste.co",
    },
    {
      date: "04 Feb 2026",
      topic: "Migration",
      title: "The four fields nobody at any practice could explain",
      readingTime: "6 min",
      href: "https://beste.co",
    },
  ],
  emptyLabel: "Nothing filed under that yet.",
};

export function Blog61({
  badge,
  heading,
  description,
  allLabel = "All",
  posts = [],
  emptyLabel,
  className,
}: Blog61Props) {
  const [topic, setTopic] = useState<string | null>(null);

  const topics = Array.from(new Set(posts.map((post) => post.topic)));
  const visible = topic ? posts.filter((post) => post.topic === topic) : posts;

  const filters = [
    { label: allLabel, value: null, count: posts.length },
    ...topics.map((name) => ({
      label: name,
      value: name,
      count: posts.filter((post) => post.topic === name).length,
    })),
  ];

  return (
    <section className={cn("bg-background py-16 md:py-24 w-full", className)}>
      <div className="mx-auto max-w-7xl px-4 md:px-6">
        {badge && <Badge23 label={badge.label} />}

        <div className="mt-6 border-t border-border pt-8 md:pt-10">
          <div className="grid gap-6 md:grid-cols-2 md:gap-12">
            {heading && (
              <h2 className="text-3xl font-light leading-[1.1] tracking-tight text-foreground md:text-5xl">
                {heading}
              </h2>
            )}
            {description && (
              <div className="flex md:justify-end">
                <p className="max-w-md text-base leading-relaxed text-muted-foreground">
                  {description}
                </p>
              </div>
            )}
          </div>
        </div>

        <div className="mt-10 flex flex-wrap gap-2">
          {filters.map((filter, index) => {
            const isActive = topic === filter.value;
            return (
              <button
                key={index}
                type="button"
                onClick={() => setTopic(filter.value)}
                aria-pressed={isActive}
                className={cn(
                  "cursor-pointer rounded-full border px-4 py-1.5 text-sm transition-colors",
                  isActive
                    ? "border-foreground bg-foreground text-background"
                    : "border-border bg-transparent text-muted-foreground hover:text-foreground"
                )}
              >
                {filter.label}
                <span className="ml-2 tabular-nums opacity-60">{filter.count}</span>
              </button>
            );
          })}
        </div>

        <div className="mt-10 grid gap-x-12 md:grid-cols-2">
          {visible.map((post, index) => (
            <Link
              key={index}
              href={post.href}
              className="flex flex-col gap-1 border-t border-border py-5 transition-colors hover:bg-muted md:flex-row md:items-baseline md:gap-6"
            >
              <span className="w-28 shrink-0 text-sm text-muted-foreground">
                {post.date}
              </span>
              <span className="flex-1 text-lg font-medium text-foreground">{post.title}</span>
              <span className="shrink-0 text-sm text-muted-foreground">{post.readingTime}</span>
            </Link>
          ))}
        </div>

        {visible.length === 0 && emptyLabel && (
          <p className="mt-10 border-t border-border pt-6 text-base text-muted-foreground">
            {emptyLabel}
          </p>
        )}
      </div>
    </section>
  );
}
```

### `registry-components/badge23/badge23.tsx`

```tsx
"use client";

import { cn } from "@/lib/utils";

type Tone = "muted" | "foreground" | "primary";

interface Badge23Props {
  /** Eyebrow label */
  label: string;
  /** Text/border tone */
  tone?: Tone;
  /** Additional classes merged onto the root */
  className?: string;
}

const toneStyles: Record<Tone, string> = {
  muted: "border-border text-muted-foreground",
  foreground: "border-foreground/30 text-foreground",
  primary: "border-primary/40 text-primary",
};

export const badge23Demo: Badge23Props = {
  label: "Product",
};

export function Badge23({ label, tone = "muted", className }: Badge23Props) {
  return (
    <span
      className={cn(
        "inline-flex items-center rounded-md border bg-transparent px-2.5 py-1 text-sm font-medium leading-none",
        toneStyles[tone],
        className
      )}
    >
      {label}
    </span>
  );
}
```
