Skip to content
All Taste components

Blog

Blog — minimal list with dates

A text-only list of posts with the date in its own column, filter tabs on top and an arrow that fills on hover.

topic filter tabs · date column · title + excerpt · hover arrow

"use client";
import { useState } from "react";
import { ArrowRight } from "lucide-react";

const topics = ["All", "Guides", "Getting paid", "Product"] as const;

const posts = [
  { title: "How to write payment terms clients actually read", excerpt: "Five lines that cut late payments without sounding strict, with examples you can copy.", topic: "Guides", href: "/notes/payment-terms", date: "Oct 8, 2026", dateTime: "2026-10-08" },
  { title: "Late fees: when to charge them and how much", excerpt: "What’s normal, what’s legal and how to bring it up with a client you want to keep.", topic: "Getting paid", href: "/notes/late-fees", date: "Sep 29, 2026", dateTime: "2026-09-29" },
  { title: "What’s new: recurring invoices for retainers", excerpt: "Bill the same client every month without lifting a finger, with prorating built in.", topic: "Product", href: "/notes/recurring", date: "Sep 17, 2026", dateTime: "2026-09-17" },
  { title: "The month-end checklist for a one-person business", excerpt: "Twenty minutes, once a month, and your accountant will never chase you again.", topic: "Guides", href: "/notes/month-end", date: "Sep 4, 2026", dateTime: "2026-09-04" },
];

export function BlogMinimalList() {
  const [topic, setTopic] = useState<(typeof topics)[number]>("All");
  const shown = topic === "All" ? posts : posts.filter((p) => p.topic === topic);
  return (
    <section aria-labelledby="notes-title" className="bg-bg px-5 py-16 font-sans sm:px-10 sm:py-20">
      <div className="mx-auto max-w-4xl">
        <h2 id="notes-title" className="text-3xl font-semibold tracking-tight text-fg sm:text-4xl">Notes</h2>
        <p className="mt-4 text-base text-fg-muted sm:text-lg">Short writing on invoicing, cash flow and running a small business.</p>

        <div role="tablist" aria-label="Filter posts" className="mt-7 flex gap-[18px] overflow-x-auto border-b border-border">
          {topics.map((t) => (
            <button key={t} type="button" role="tab" aria-selected={topic === t} onClick={() => setTopic(t)}
              className="-mb-px h-9 shrink-0 border-b-2 border-transparent text-[13px] font-medium text-fg-muted transition aria-selected:border-primary aria-selected:text-fg">
              {t}
            </button>
          ))}
        </div>

        <ul className="mt-2 border-t border-border">
          {shown.map((p) => (
            <li key={p.href}>
              <a href={p.href} className="group grid grid-cols-[1fr_auto] items-center gap-x-8 gap-y-1.5 border-b border-border py-6 sm:grid-cols-[140px_1fr_auto]">
                <time dateTime={p.dateTime} className="col-span-full text-[13px] tabular-nums text-fg-subtle sm:col-span-1 sm:self-start sm:pt-1">{p.date}</time>
                <div>
                  <p className="text-lg font-semibold leading-snug tracking-tight text-fg transition group-hover:text-primary">{p.title}</p>
                  <p className="mt-1.5 text-sm leading-relaxed text-fg-muted">{p.excerpt}</p>
                </div>
                <span className="grid size-10 place-items-center rounded-full border border-border text-fg-muted transition group-hover:border-primary group-hover:bg-primary group-hover:text-primary-fg" aria-hidden>
                  <ArrowRight className="size-4" />
                </span>
              </a>
            </li>
          ))}
        </ul>
      </div>
    </section>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.