Blog Post 1: article with contents and progress
A long-form article page: centered title and author row, cover, comfortable prose with pull quote and code, sticky table of contents with scroll spy, reading progress bar, author card and related posts.
// Ballmac UI: Blog Post 1. https://ui.ballmac.com/blocks/blog-post-1
"use client"
import * as React from "react"
import { Clock } from "lucide-react"
import { BlogCover } from "@/components/ballmac/blocks/blog-1/blog-1"
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ballmac/breadcrumb"
import { CopyButton } from "@/components/ballmac/copy-button"
import { ScrollProgress } from "@/components/ballmac/scroll-progress"
import { TableOfContents, type TocItem } from "@/components/ballmac/table-of-contents"
import { cn } from "@/lib/utils"
import { useLocale } from "@/lib/ballmac/i18n"
import { Media, type MediaSource } from "@/components/ballmac/media"
type BlogPostRelated = { title: string; category: string; href: string; readMinutes: number }
type BlogPost1Props = Omit<React.ComponentProps<"article">, "title"> & {
/** Topic shown in the breadcrumb and above the title. */
category?: string
/** The article title. */
title?: string
/** One or two sentences under the title. */
subtitle?: string
/** Publication date as an ISO string (YYYY-MM-DD), shown in UTC. */
date?: string
/** Estimated minutes to read. */
readMinutes?: number
/** Author's name. */
author?: string
/** Author's role. */
role?: string
/** One sentence about the author, shown in the card after the article. */
authorBio?: string
/** Absolute link copied by the share button. */
url?: string
/** Cover photo: an image URL (give it imageAlt), an object with alt text and a dark-mode file, or your own element. A generated cover is drawn when omitted. */
image?: MediaSource
/** Describes `image` when it is a plain URL. */
imageAlt?: string
/** Which generated cover to draw (0 to 4). */
cover?: number
/** Links for "On this page". The ids must match headings in `children`. */
toc?: TocItem[]
/** Pixels reserved for a sticky site header, for the contents list and anchor scrolling. */
stickyOffset?: number
/** More posts to read. Pass an empty array to hide the section. */
related?: BlogPostRelated[]
/** Link back to the blog index. */
blogHref?: string
/** Locale for the date. Fixed by default so server and browser match. */
locale?: string
/** The article body. Plain h2, h3, p, ul, ol, blockquote, pre and a elements are styled for you. */
children?: React.ReactNode
}
const proseClass = cn(
"text-foreground",
"[&_h2]:mt-14 [&_h2]:scroll-mt-28 [&_h2]:text-2xl [&_h2]:font-semibold [&_h2]:tracking-[-0.025em] [&_h2]:text-balance sm:[&_h2]:text-[1.7rem]",
"[&_h3]:mt-10 [&_h3]:scroll-mt-28 [&_h3]:text-xl [&_h3]:font-semibold [&_h3]:tracking-[-0.02em]",
"[&_p]:mt-5 [&_p]:text-[1.0625rem] [&_p]:leading-8 [&_p]:text-pretty",
"[&_ul]:mt-5 [&_ul]:list-disc [&_ul]:space-y-2 [&_ul]:ps-6 [&_ul]:text-[1.0625rem] [&_ul]:leading-8 [&_ul]:marker:text-muted-foreground",
"[&_ol]:mt-5 [&_ol]:list-decimal [&_ol]:space-y-2 [&_ol]:ps-6 [&_ol]:text-[1.0625rem] [&_ol]:leading-8 [&_ol]:marker:text-muted-foreground",
"[&_a]:font-medium [&_a]:underline [&_a]:decoration-border [&_a]:underline-offset-4 hover:[&_a]:decoration-foreground",
"[&_blockquote]:my-10 [&_blockquote]:border-s-2 [&_blockquote]:border-foreground [&_blockquote]:ps-6 [&_blockquote]:text-2xl [&_blockquote]:leading-snug [&_blockquote]:font-medium [&_blockquote]:tracking-[-0.02em] [&_blockquote]:text-balance",
"[&_pre]:mt-6 [&_pre]:overflow-x-auto [&_pre]:rounded-2xl [&_pre]:border [&_pre]:bg-muted/50 [&_pre]:p-5 [&_pre]:font-mono [&_pre]:text-[13px] [&_pre]:leading-6",
"[&_code:not(pre_code)]:rounded-md [&_code:not(pre_code)]:bg-muted [&_code:not(pre_code)]:px-1.5 [&_code:not(pre_code)]:py-0.5 [&_code:not(pre_code)]:font-mono [&_code:not(pre_code)]:text-[0.9em]",
"[&_hr]:my-12"
)
const defaultToc: TocItem[] = [
{ id: "the-problem", title: "The problem with five days", level: 2 },
{ id: "three-changes", title: "Three changes that mattered", level: 2 },
{ id: "approvals-in-flight", title: "Approvals in flight", level: 3 },
{ id: "one-source-of-truth", title: "One source of truth", level: 3 },
{ id: "what-we-measured", title: "What we measured", level: 2 },
{ id: "what-is-next", title: "What is next", level: 2 },
]
function DefaultBody() {
return (
<>
<p>
For years our own books took five working days to close. Nobody was slow; the work was scattered across inboxes, spreadsheets and a shared drive with
a folder structure only two people understood. This is the story of how we got it to one day, and what we changed in the product along the way.
</p>
<h2 id="the-problem">The problem with five days</h2>
<p>
When we mapped the close, only about a fifth of the elapsed time was actual work. The rest was waiting: for a receipt, for an approval, for someone
to notice that a number looked off. Every wait was small, and together they were the whole week.
</p>
<blockquote>Most of a close is waiting. The fastest teams are not working harder, they are waiting less.</blockquote>
<h2 id="three-changes">Three changes that mattered</h2>
<p>We resisted the urge to automate everything. Three changes did almost all of the work.</p>
<h3 id="approvals-in-flight">Approvals in flight</h3>
<p>
Instead of a monthly approval sweep, approvals happen the moment an invoice is coded. A reviewer sees a small queue each morning rather than a
mountain on day three.
</p>
<h3 id="one-source-of-truth">One source of truth</h3>
<p>
We deleted the side spreadsheets. Every figure in the close now links back to the transaction that produced it, so questions are answered by
clicking, not by emailing.
</p>
<pre>
<code>{`close.status("2026-09")\n// → { days: 1, open: 0, flagged: 2 }`}</code>
</pre>
<h2 id="what-we-measured">What we measured</h2>
<ul>
<li>Median time to close fell from 5.1 days to 1.2 days over four months.</li>
<li>Late adjustments dropped by about two thirds.</li>
<li>The finance team reported spending most of their time on analysis rather than chasing.</li>
</ul>
<h2 id="what-is-next">What is next</h2>
<p>
We are turning the pieces that helped us most into defaults for everyone. If you want to try them early, <a href="#">join the beta</a> and tell us what
your own close looks like.
</p>
</>
)
}
const defaultRelated: BlogPostRelated[] = [
{ title: "Designing calm software for busy teams", category: "Design", href: "#", readMinutes: 6 },
{ title: "What 2,000 invoices taught us about getting paid", category: "Research", href: "#", readMinutes: 7 },
{ title: "Inside our new audit log", category: "Engineering", href: "#", readMinutes: 11 },
]
function BlogPost1({
category = "Product",
title = "How we cut month-end close from five days to one",
subtitle = "Three workflow changes, one deleted spreadsheet and the dashboards that made them stick.",
date = "2026-09-24",
readMinutes = 8,
author = "Priya Raman",
role = "CEO",
authorBio = "Priya leads Acme. Before that she ran finance operations at two growth-stage companies and has closed more books than she cares to count.",
url = "https://acme.com/blog/month-end-close",
image,
imageAlt,
cover = 0,
toc = defaultToc,
stickyOffset = 24,
related = defaultRelated,
blogHref = "#",
locale,
children,
className,
...props
}: BlogPost1Props) {
const defaultLocale = useLocale()
locale ??= defaultLocale
const formatted = new Intl.DateTimeFormat(locale, { dateStyle: "long", timeZone: "UTC" }).format(new Date(`${date}T00:00:00Z`))
const initials = author.split(" ").map((w) => w[0]).join("").slice(0, 2)
return (
<>
<ScrollProgress />
<article data-slot="blog-post-1" className={cn("mx-auto max-w-6xl px-4 py-12 sm:px-6 md:py-20", className)} {...props}>
<header className="mx-auto max-w-3xl text-center">
<Breadcrumb className="flex justify-center">
<BreadcrumbList>
<BreadcrumbItem><BreadcrumbLink href={blogHref}>Blog</BreadcrumbLink></BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem><BreadcrumbPage>{category}</BreadcrumbPage></BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<h1 className="mt-6 text-4xl font-semibold tracking-[-0.04em] text-balance sm:text-5xl lg:text-6xl lg:leading-[1.05]">{title}</h1>
<p className="text-muted-foreground mt-5 text-lg text-pretty sm:text-xl">{subtitle}</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-x-4 gap-y-3 text-sm">
<div className="flex items-center gap-3">
<span aria-hidden="true" className="bg-chart-1/25 flex size-9 items-center justify-center rounded-full text-xs font-semibold">{initials}</span>
<p className="text-start leading-tight">
<span className="font-medium">{author}</span>
<span className="text-muted-foreground block">{role}</span>
</p>
</div>
<span aria-hidden="true" className="bg-border hidden h-6 w-px sm:block" />
<p className="text-muted-foreground flex items-center gap-2">
<time dateTime={date}>{formatted}</time>
<span aria-hidden="true">·</span>
<span className="inline-flex items-center gap-1"><Clock className="size-3.5" aria-hidden="true" />{readMinutes} min read</span>
</p>
<CopyButton value={url} label="Copy link" copiedLabel="Link copied" variant="outline" size="sm" />
</div>
</header>
<div className="mx-auto mt-12 max-w-5xl overflow-hidden rounded-3xl border">
{image ? (
<Media media={image} alt={imageAlt} aspect="21/9" priority className="w-full" />
) : (
<BlogCover variant={cover} className="aspect-[16/9] w-full sm:aspect-[21/9]" />
)}
</div>
<div className="mx-auto mt-12 grid max-w-5xl gap-12 lg:grid-cols-[minmax(0,42rem)_14rem] lg:justify-between">
<div className="min-w-0">
<div className={proseClass}>{children ?? <DefaultBody />}</div>
<aside aria-label="About the author" className="bg-muted/40 mt-16 flex gap-4 rounded-3xl border p-6">
<span aria-hidden="true" className="bg-chart-1/25 flex size-12 shrink-0 items-center justify-center rounded-full text-sm font-semibold">{initials}</span>
<div>
<p className="font-semibold">Written by {author}</p>
<p className="text-muted-foreground mt-1 text-sm leading-relaxed text-pretty">{authorBio}</p>
</div>
</aside>
</div>
{toc.length > 0 && (
<div className="hidden lg:block">
<div className="sticky" style={{ top: stickyOffset }}>
<TableOfContents items={toc} offset={stickyOffset + 72} />
</div>
</div>
)}
</div>
{related.length > 0 && (
<section aria-labelledby="related-heading" className="mx-auto mt-20 max-w-5xl border-t pt-10">
<h2 id="related-heading" className="text-lg font-semibold tracking-tight">Keep reading</h2>
<ul className="mt-6 grid gap-4 sm:grid-cols-3">
{related.map((r) => (
<li key={r.title}>
<a href={r.href} className="group/related hover:bg-accent focus-visible:ring-ring/50 block h-full rounded-2xl border p-5 outline-none transition-colors focus-visible:ring-[3px]">
<span className="text-muted-foreground text-xs font-medium tracking-wide uppercase">{r.category}</span>
<span className="mt-2 block leading-snug font-semibold text-balance">{r.title}</span>
<span className="text-muted-foreground mt-4 block text-sm">{r.readMinutes} min read</span>
</a>
</li>
))}
</ul>
</section>
)}
</article>
</>
)
}
export { BlogPost1, type BlogPost1Props, type BlogPostRelated }Installation
$ pnpm dlx shadcn@latest add @ballmac/blog-post-1Install the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/blog-1 @ballmac/breadcrumb @ballmac/copy-button @ballmac/scroll-progress @ballmac/table-of-contents @ballmac/i18n @ballmac/mediaCopy the source into your project.
components/ballmac/blocks/blog-post-1/blog-post-1.tsx// Ballmac UI: Blog Post 1. https://ui.ballmac.com/blocks/blog-post-1 "use client" import * as React from "react" import { Clock } from "lucide-react" import { BlogCover } from "@/components/ballmac/blocks/blog-1/blog-1" import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ballmac/breadcrumb" import { CopyButton } from "@/components/ballmac/copy-button" import { ScrollProgress } from "@/components/ballmac/scroll-progress" import { TableOfContents, type TocItem } from "@/components/ballmac/table-of-contents" import { cn } from "@/lib/utils" import { useLocale } from "@/lib/ballmac/i18n" import { Media, type MediaSource } from "@/components/ballmac/media" type BlogPostRelated = { title: string; category: string; href: string; readMinutes: number } type BlogPost1Props = Omit<React.ComponentProps<"article">, "title"> & { /** Topic shown in the breadcrumb and above the title. */ category?: string /** The article title. */ title?: string /** One or two sentences under the title. */ subtitle?: string /** Publication date as an ISO string (YYYY-MM-DD), shown in UTC. */ date?: string /** Estimated minutes to read. */ readMinutes?: number /** Author's name. */ author?: string /** Author's role. */ role?: string /** One sentence about the author, shown in the card after the article. */ authorBio?: string /** Absolute link copied by the share button. */ url?: string /** Cover photo: an image URL (give it imageAlt), an object with alt text and a dark-mode file, or your own element. A generated cover is drawn when omitted. */ image?: MediaSource /** Describes `image` when it is a plain URL. */ imageAlt?: string /** Which generated cover to draw (0 to 4). */ cover?: number /** Links for "On this page". The ids must match headings in `children`. */ toc?: TocItem[] /** Pixels reserved for a sticky site header, for the contents list and anchor scrolling. */ stickyOffset?: number /** More posts to read. Pass an empty array to hide the section. */ related?: BlogPostRelated[] /** Link back to the blog index. */ blogHref?: string /** Locale for the date. Fixed by default so server and browser match. */ locale?: string /** The article body. Plain h2, h3, p, ul, ol, blockquote, pre and a elements are styled for you. */ children?: React.ReactNode } const proseClass = cn( "text-foreground", "[&_h2]:mt-14 [&_h2]:scroll-mt-28 [&_h2]:text-2xl [&_h2]:font-semibold [&_h2]:tracking-[-0.025em] [&_h2]:text-balance sm:[&_h2]:text-[1.7rem]", "[&_h3]:mt-10 [&_h3]:scroll-mt-28 [&_h3]:text-xl [&_h3]:font-semibold [&_h3]:tracking-[-0.02em]", "[&_p]:mt-5 [&_p]:text-[1.0625rem] [&_p]:leading-8 [&_p]:text-pretty", "[&_ul]:mt-5 [&_ul]:list-disc [&_ul]:space-y-2 [&_ul]:ps-6 [&_ul]:text-[1.0625rem] [&_ul]:leading-8 [&_ul]:marker:text-muted-foreground", "[&_ol]:mt-5 [&_ol]:list-decimal [&_ol]:space-y-2 [&_ol]:ps-6 [&_ol]:text-[1.0625rem] [&_ol]:leading-8 [&_ol]:marker:text-muted-foreground", "[&_a]:font-medium [&_a]:underline [&_a]:decoration-border [&_a]:underline-offset-4 hover:[&_a]:decoration-foreground", "[&_blockquote]:my-10 [&_blockquote]:border-s-2 [&_blockquote]:border-foreground [&_blockquote]:ps-6 [&_blockquote]:text-2xl [&_blockquote]:leading-snug [&_blockquote]:font-medium [&_blockquote]:tracking-[-0.02em] [&_blockquote]:text-balance", "[&_pre]:mt-6 [&_pre]:overflow-x-auto [&_pre]:rounded-2xl [&_pre]:border [&_pre]:bg-muted/50 [&_pre]:p-5 [&_pre]:font-mono [&_pre]:text-[13px] [&_pre]:leading-6", "[&_code:not(pre_code)]:rounded-md [&_code:not(pre_code)]:bg-muted [&_code:not(pre_code)]:px-1.5 [&_code:not(pre_code)]:py-0.5 [&_code:not(pre_code)]:font-mono [&_code:not(pre_code)]:text-[0.9em]", "[&_hr]:my-12" ) const defaultToc: TocItem[] = [ { id: "the-problem", title: "The problem with five days", level: 2 }, { id: "three-changes", title: "Three changes that mattered", level: 2 }, { id: "approvals-in-flight", title: "Approvals in flight", level: 3 }, { id: "one-source-of-truth", title: "One source of truth", level: 3 }, { id: "what-we-measured", title: "What we measured", level: 2 }, { id: "what-is-next", title: "What is next", level: 2 }, ] function DefaultBody() { return ( <> <p> For years our own books took five working days to close. Nobody was slow; the work was scattered across inboxes, spreadsheets and a shared drive with a folder structure only two people understood. This is the story of how we got it to one day, and what we changed in the product along the way. </p> <h2 id="the-problem">The problem with five days</h2> <p> When we mapped the close, only about a fifth of the elapsed time was actual work. The rest was waiting: for a receipt, for an approval, for someone to notice that a number looked off. Every wait was small, and together they were the whole week. </p> <blockquote>Most of a close is waiting. The fastest teams are not working harder, they are waiting less.</blockquote> <h2 id="three-changes">Three changes that mattered</h2> <p>We resisted the urge to automate everything. Three changes did almost all of the work.</p> <h3 id="approvals-in-flight">Approvals in flight</h3> <p> Instead of a monthly approval sweep, approvals happen the moment an invoice is coded. A reviewer sees a small queue each morning rather than a mountain on day three. </p> <h3 id="one-source-of-truth">One source of truth</h3> <p> We deleted the side spreadsheets. Every figure in the close now links back to the transaction that produced it, so questions are answered by clicking, not by emailing. </p> <pre> <code>{`close.status("2026-09")\n// → { days: 1, open: 0, flagged: 2 }`}</code> </pre> <h2 id="what-we-measured">What we measured</h2> <ul> <li>Median time to close fell from 5.1 days to 1.2 days over four months.</li> <li>Late adjustments dropped by about two thirds.</li> <li>The finance team reported spending most of their time on analysis rather than chasing.</li> </ul> <h2 id="what-is-next">What is next</h2> <p> We are turning the pieces that helped us most into defaults for everyone. If you want to try them early, <a href="#">join the beta</a> and tell us what your own close looks like. </p> </> ) } const defaultRelated: BlogPostRelated[] = [ { title: "Designing calm software for busy teams", category: "Design", href: "#", readMinutes: 6 }, { title: "What 2,000 invoices taught us about getting paid", category: "Research", href: "#", readMinutes: 7 }, { title: "Inside our new audit log", category: "Engineering", href: "#", readMinutes: 11 }, ] function BlogPost1({ category = "Product", title = "How we cut month-end close from five days to one", subtitle = "Three workflow changes, one deleted spreadsheet and the dashboards that made them stick.", date = "2026-09-24", readMinutes = 8, author = "Priya Raman", role = "CEO", authorBio = "Priya leads Acme. Before that she ran finance operations at two growth-stage companies and has closed more books than she cares to count.", url = "https://acme.com/blog/month-end-close", image, imageAlt, cover = 0, toc = defaultToc, stickyOffset = 24, related = defaultRelated, blogHref = "#", locale, children, className, ...props }: BlogPost1Props) { const defaultLocale = useLocale() locale ??= defaultLocale const formatted = new Intl.DateTimeFormat(locale, { dateStyle: "long", timeZone: "UTC" }).format(new Date(`${date}T00:00:00Z`)) const initials = author.split(" ").map((w) => w[0]).join("").slice(0, 2) return ( <> <ScrollProgress /> <article data-slot="blog-post-1" className={cn("mx-auto max-w-6xl px-4 py-12 sm:px-6 md:py-20", className)} {...props}> <header className="mx-auto max-w-3xl text-center"> <Breadcrumb className="flex justify-center"> <BreadcrumbList> <BreadcrumbItem><BreadcrumbLink href={blogHref}>Blog</BreadcrumbLink></BreadcrumbItem> <BreadcrumbSeparator /> <BreadcrumbItem><BreadcrumbPage>{category}</BreadcrumbPage></BreadcrumbItem> </BreadcrumbList> </Breadcrumb> <h1 className="mt-6 text-4xl font-semibold tracking-[-0.04em] text-balance sm:text-5xl lg:text-6xl lg:leading-[1.05]">{title}</h1> <p className="text-muted-foreground mt-5 text-lg text-pretty sm:text-xl">{subtitle}</p> <div className="mt-8 flex flex-wrap items-center justify-center gap-x-4 gap-y-3 text-sm"> <div className="flex items-center gap-3"> <span aria-hidden="true" className="bg-chart-1/25 flex size-9 items-center justify-center rounded-full text-xs font-semibold">{initials}</span> <p className="text-start leading-tight"> <span className="font-medium">{author}</span> <span className="text-muted-foreground block">{role}</span> </p> </div> <span aria-hidden="true" className="bg-border hidden h-6 w-px sm:block" /> <p className="text-muted-foreground flex items-center gap-2"> <time dateTime={date}>{formatted}</time> <span aria-hidden="true">·</span> <span className="inline-flex items-center gap-1"><Clock className="size-3.5" aria-hidden="true" />{readMinutes} min read</span> </p> <CopyButton value={url} label="Copy link" copiedLabel="Link copied" variant="outline" size="sm" /> </div> </header> <div className="mx-auto mt-12 max-w-5xl overflow-hidden rounded-3xl border"> {image ? ( <Media media={image} alt={imageAlt} aspect="21/9" priority className="w-full" /> ) : ( <BlogCover variant={cover} className="aspect-[16/9] w-full sm:aspect-[21/9]" /> )} </div> <div className="mx-auto mt-12 grid max-w-5xl gap-12 lg:grid-cols-[minmax(0,42rem)_14rem] lg:justify-between"> <div className="min-w-0"> <div className={proseClass}>{children ?? <DefaultBody />}</div> <aside aria-label="About the author" className="bg-muted/40 mt-16 flex gap-4 rounded-3xl border p-6"> <span aria-hidden="true" className="bg-chart-1/25 flex size-12 shrink-0 items-center justify-center rounded-full text-sm font-semibold">{initials}</span> <div> <p className="font-semibold">Written by {author}</p> <p className="text-muted-foreground mt-1 text-sm leading-relaxed text-pretty">{authorBio}</p> </div> </aside> </div> {toc.length > 0 && ( <div className="hidden lg:block"> <div className="sticky" style={{ top: stickyOffset }}> <TableOfContents items={toc} offset={stickyOffset + 72} /> </div> </div> )} </div> {related.length > 0 && ( <section aria-labelledby="related-heading" className="mx-auto mt-20 max-w-5xl border-t pt-10"> <h2 id="related-heading" className="text-lg font-semibold tracking-tight">Keep reading</h2> <ul className="mt-6 grid gap-4 sm:grid-cols-3"> {related.map((r) => ( <li key={r.title}> <a href={r.href} className="group/related hover:bg-accent focus-visible:ring-ring/50 block h-full rounded-2xl border p-5 outline-none transition-colors focus-visible:ring-[3px]"> <span className="text-muted-foreground text-xs font-medium tracking-wide uppercase">{r.category}</span> <span className="mt-2 block leading-snug font-semibold text-balance">{r.title}</span> <span className="text-muted-foreground mt-4 block text-sm">{r.readMinutes} min read</span> </a> </li> ))} </ul> </section> )} </article> </> ) } export { BlogPost1, type BlogPost1Props, type BlogPostRelated }Update the import paths to match your project setup.
Usage
import { BlogPost1 } from "@/components/ballmac/blocks/blog-post-1/blog-post-1"
export default function BlogPost1Demo() {
return <BlogPost1 />
}Dependencies
- npm
- lucide-react
Use with AI
An article template. Pass title, subtitle, author, date and readMinutes, give the body as children (plain h2, h3, p, ul, blockquote and pre are styled), and list the headings in toc. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Blog Post 1: article with contents and progress (@ballmac/blog-post-1) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Blog posts, guides and announcements
- Any long reading page
Not for
- Documentation with a left sidebar (use your docs layout)
Registry JSON: https://ui.ballmac.com/r/blog-post-1.json
Free to use in personal and commercial projects.
More blog blocks
Blog 1: featured post with filterable grid
A blog index with a large featured post, topic filter chips and a three-column grid. Posts without a photo get a generated cover drawn from theme tokens, in five compositions.
Blog Pro 1: journal home with filters
A featured post, category chips, search and a grid of posts with a Load more button. A polite count says how many posts match.
Blog Pro 2: article page with contents
A long-form article with a reading progress bar, a sticky contents list that follows your place, copy and share buttons, pull quotes and an author box.
Blog Pro 3: magazine front page
A large lead story, two medium stories and four numbered headlines in a bordered strip.