Release Timeline
A dated changelog on a vertical rail with added/changed/fixed tags and machine-readable <time> values.
Kicker, headline, standfirst, byline and share row, with the opening prose and a pull quote included — the type scale of a post, decided once instead of per-article.
components/article-header.tsxnpx hoverlab add article-header
Or over MCP, from your editor's agent — no account needed.
Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.
Start a page with this section — add more, order them, and leave with the page source.
Flags are borrowed complexity — this is what it cost to pay ours back, and the two rules that keep the debt from growing again.
The pull request was titled “remove stale flags, part 1 of 8” and it deleted more code than the previous quarter had added. Our flag service listed 312 active flags; production telemetry showed 41 of them had evaluated to the same value for every user since at least January. The rest were split roughly evenly between experiments nobody remembered starting and kill switches for services we had already decommissioned.
“A feature flag is a loan. The interest is every engineer who has to reason about both branches forever.”
Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.
'use client'
/**
* <ArticleHeader> — the opening screen of a single article.
*
* The byline sits between the standfirst and the prose, not in a sidebar:
* on the one screen where a reader decides whether to commit, "who wrote
* this and how long will it take" is part of the pitch, not metadata.
*
* The share row includes copy-link first because that is what people
* actually do with articles worth keeping — the X and LinkedIn buttons are
* mostly there so the copy button has company. The copy handler feature-
* detects `navigator.clipboard` and simply hides itself where the API is
* unavailable, rather than rendering a button that silently fails.
*
* THE DETECTION HAPPENS IN AN EFFECT, NOT DURING RENDER
*
* `typeof navigator !== 'undefined'` read during render is false on the
* server and true in the browser, so the server sent the X link where the
* client wanted a copy button and React threw the whole subtree away with
* "Hydration failed". It was invisible on the page — the regenerated tree
* looks right — and only showed up as a console error, which is how it
* survived in a shipped block.
*
* Reading it in `useEffect` means the first client render matches the
* server exactly (no button), and the button appears on the pass after
* hydration. That is the correct shape for every browser-capability check
* in a server-rendered component, and it is worth copying: the same
* one-line detection is a hydration bug anywhere it is read during render.
*/
import * as React from 'react'
import { Link2, Check, Twitter, Linkedin } from 'lucide-react'
export interface ArticleHeaderProps {
category?: string
title?: string
standfirst?: string
author?: string
role?: string
/** ISO date — `2026-07-21`. */
date?: string
readMinutes?: number
className?: string
}
export function ArticleHeader({
category = 'Engineering',
title = 'The day we deleted 40,000 lines of feature flags',
standfirst = 'Flags are borrowed complexity — this is what it cost to pay ours back, and the two rules that keep the debt from growing again.',
author = 'Priya Raman',
role = 'Staff Engineer',
date = '2026-07-21',
readMinutes = 12,
className = '',
}: ArticleHeaderProps) {
const [copied, setCopied] = React.useState(false)
// Starts false so the server's HTML and the first client render agree.
const [canCopy, setCanCopy] = React.useState(false)
React.useEffect(() => {
setCanCopy(Boolean(navigator.clipboard))
}, [])
const copyLink = async () => {
try {
await navigator.clipboard.writeText(window.location.href)
setCopied(true)
window.setTimeout(() => setCopied(false), 2000)
} catch {
// Clipboard permission denied — leave the button in its idle state.
}
}
return (
<header className={`mx-auto w-full max-w-3xl px-4 py-16 sm:px-6 ${className}`}>
<a
href="#"
className="text-sm font-semibold uppercase tracking-wide text-primary transition-colors hover:text-primary/80"
>
{category}
</a>
<h1 className="mt-4 text-balance break-words text-4xl font-bold tracking-tight sm:text-5xl">{title}</h1>
<p className="mt-5 text-pretty text-lg leading-relaxed text-muted-foreground">{standfirst}</p>
<div className="mt-8 flex flex-wrap items-center justify-between gap-4 border-y border-border/60 py-4">
<div className="flex items-center gap-3">
<span
aria-hidden
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-bold text-primary"
>
{author
.split(' ')
.slice(0, 2)
.map((w) => w[0] ?? '')
.join('')
.toUpperCase()}
</span>
<div className="text-sm">
<p className="font-semibold">{author}</p>
<p className="text-muted-foreground">
{role} ·{' '}
<time dateTime={date}>
{new Date(`${date}T00:00:00Z`).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})}
</time>{' '}
· {readMinutes} min read
</p>
</div>
</div>
<div className="flex items-center gap-2">
{canCopy ? (
<button
type="button"
onClick={copyLink}
className="inline-flex items-center gap-1.5 rounded-lg border border-border/60 bg-background px-3 py-1.5 text-xs font-medium transition-colors hover:bg-muted"
>
{copied ? (
<Check aria-hidden className="h-3.5 w-3.5 text-emerald-500" />
) : (
<Link2 aria-hidden className="h-3.5 w-3.5" />
)}
{copied ? 'Copied' : 'Copy link'}
</button>
) : null}
<a
href="#"
aria-label="Share on X"
className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border/60 bg-background text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
<Twitter aria-hidden className="h-3.5 w-3.5" />
</a>
<a
href="#"
aria-label="Share on LinkedIn"
className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border/60 bg-background text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
<Linkedin aria-hidden className="h-3.5 w-3.5" />
</a>
</div>
</div>
<div className="mt-10 space-y-8">
<p className="text-pretty leading-relaxed text-foreground/90">
The pull request was titled “remove stale flags, part 1 of
8” and it deleted more code than the previous quarter had
added. Our flag service listed 312 active flags; production
telemetry showed 41 of them had evaluated to the same value for
every user since at least January. The rest were split roughly
evenly between experiments nobody remembered starting and kill
switches for services we had already decommissioned.
</p>
<blockquote className="border-s-4 border-primary/60 ps-6">
<p className="text-pretty text-xl font-semibold leading-relaxed tracking-tight text-foreground">
“A feature flag is a loan. The interest is every engineer
who has to reason about both branches forever.”
</p>
</blockquote>
</div>
</header>
)
}
bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.Drop it at components/article-header.tsx and import it where you need the section:
import { ArticleHeader } from '@/components/article-header'6 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.
Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.
| Prop | Type | Default |
|---|---|---|
category | string | 'Engineering' |
title | string | 'The day we deleted 40,000 lines of feature f… |
standfirst | string | — |
author | string | 'Priya Raman' |
role | string | 'Staff Engineer' |
dateISO date — `2026-07-21`. | string | '2026-07-21' |
readMinutes | number | 12 |
className | string | '' |
The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.
This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.
<!--
Article Header & Byline — markup from the Hoverlab catalog.
This is the block rendered once to HTML and wrapped as a component
file. It is not a port of the React source: the Tailwind classes carry
the design, which is the part that took the work, and they are the same
in every framework.
This block is interactive in React and the handlers are NOT here.
Buttons, toggles and menus render in their initial state and do
nothing until you wire them up.
-->
<header class="mx-auto w-full max-w-3xl px-4 py-16 sm:px-6 ">
<a href="#" class="text-sm font-semibold uppercase tracking-wide text-primary transition-colors hover:text-primary/80">Engineering</a>
<h1 class="mt-4 text-balance break-words text-4xl font-bold tracking-tight sm:text-5xl">The day we deleted 40,000 lines of feature flags</h1>
<p class="mt-5 text-pretty text-lg leading-relaxed text-muted-foreground">Flags are borrowed complexity — this is what it cost to pay ours back, and the two rules that keep the debt from growing again.</p>
<div class="mt-8 flex flex-wrap items-center justify-between gap-4 border-y border-border/60 py-4">
<div class="flex items-center gap-3">
<span aria-hidden="true" class="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-bold text-primary">PR</span>
<div class="text-sm">
<p class="font-semibold">Priya Raman</p>
<p class="text-muted-foreground">Staff Engineer · <time dateTime="2026-07-21">July 21, 2026</time> · 12 min read</p>
</div>
</div>
<div class="flex items-center gap-2">
<a href="#" aria-label="Share on X" class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border/60 bg-background text-muted-foreground transition-colors hover:bg-muted hover:text-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-twitter h-3.5 w-3.5" aria-hidden="true">
<path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path>
</svg>
</a>
<a href="#" aria-label="Share on LinkedIn" class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border/60 bg-background text-muted-foreground transition-colors hover:bg-muted hover:text-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-linkedin h-3.5 w-3.5" aria-hidden="true">
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
<rect width="4" height="12" x="2" y="9"></rect>
<circle cx="4" cy="4" r="2"></circle>
</svg>
</a>
</div>
</div>
<div class="mt-10 space-y-8">
<p class="text-pretty leading-relaxed text-foreground/90">The pull request was titled “remove stale flags, part 1 of 8” and it deleted more code than the previous quarter had added. Our flag service listed 312 active flags; production telemetry showed 41 of them had evaluated to the same value for every user since at least January. The rest were split roughly evenly between experiments nobody remembered starting and kill switches for services we had already decommissioned.</p>
<blockquote class="border-s-4 border-primary/60 ps-6">
<p class="text-pretty text-xl font-semibold leading-relaxed tracking-tight text-foreground">“A feature flag is a loan. The interest is every engineer who has to reason about both branches forever.”</p>
</blockquote>
</div>
</header>
What each framework gets across the whole catalog — effects convert properly; this rung is markup.
The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.
Want the whole screen instead of this one section? Open a page and copy it entire.
All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.
Browse Content & BlogCopying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.
A dated changelog on a vertical rail with added/changed/fixed tags and machine-readable <time> values.
Shipped / in progress / planned in three columns, with status carried by icon as well as colour.
A featured post at full width above a card grid, with drawn covers built from tokens — a blog index with no image pipeline and no broken-thumbnail state.
A grid of case-study cards that lead with the metric rather than the company name, each one linking out to the full write-up — the block a customer-stories section or a home page needed and only had a full case-study page for.
An about-page team section on initials avatars rather than photos, so shipping it never waits on headshots and there is no broken-image state to design.
Openings grouped by department with location, type and salary on every row — candidates self-select by craft first, and a flat list makes them scan for it.
Sidebar, article and on-this-page rail — each column answers a different question, and they collapse on mobile in the order that keeps the article alive.
Method badge, path, parameter table and a request/response pair kept side by side — an example request whose response is a scroll away gets pasted blind.
One install command behind npm/pnpm/yarn/bun tabs with a working copy button — every reader uses exactly one package manager, so showing all four wastes three lines.
A CV as a document rather than a timeline of cards — one column, dates on the end edge, and a print layer that puts it on paper without a separate PDF to keep in step.
The watch screen with no playback in it: a ratio-locked stage, a control bar drawn down to the buffered layer, and an up-next rail. Drop a video element in and wire your own player.