Feature tabs block
BlockA short feature tour: features beside one stage, with a thin rule that fills and moves on. It waits while hovered, focused or off screen; choosing a feature or Pause stops it.
pnpm dlx shadcn@latest add https://caliperui.site/r/feature-tabs-block.jsonHow Halden closes the month
Every account reconciled before lunch
Bank feeds, rules and approvals in one place, so the close is a review rather than a rebuild.
| Mercury ••4410 | 2,184 matched | $412,880.12 |
| Stripe payouts | 96 matched | $88,214.50 |
| Amex ••1002 | 41 to review | $9,120.07 |
When to use #
Use it for
- The middle of a landing page with three to five features that each have a screen.
- Replacing a carousel that hides content behind arrows.
Skip it when
- Features without a picture of their own; use the feature rows.
- More than five features; the tour gets long.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/feature-tabs-block.json
Adds its 4 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { FeatureTabsBlock } from "@/components/ui/feature-tabs-block" import { Sparkline } from "@/components/ui/sparkline" import { Switch } from "@/components/ui/switch" function MiniLedger({ rows }: { rows: [string, string, string][] }) { return ( <table className="w-full text-left text-[13px]"> <tbody> {rows.map(([a, b, c]) => ( <tr key={a} className="border-b border-border last:border-0"> <td className="py-2.5 font-medium">{a}</td> <td className="py-2.5 text-muted-foreground">{b}</td> <td className="py-2.5 text-right tabular-nums">{c}</td> </tr> ))} </tbody> </table> ); } export function FeatureTabsBlockDemo() { return ( <FeatureTabsBlock eyebrow="How Halden closes the month" title="Every account reconciled before lunch" lead="Bank feeds, rules and approvals in one place, so the close is a review rather than a rebuild." features={[ { title: "Bank feeds that match themselves", body: "Transactions arrive overnight and match to invoices and bills by amount, date and payee. You review the few that don't.", stageTitle: "halden.app/reconcile", stage: <MiniLedger rows={[["Mercury ••4410", "2,184 matched", "$412,880.12"], ["Stripe payouts", "96 matched", "$88,214.50"], ["Amex ••1002", "41 to review", "$9,120.07"]]} />, }, { title: "Rules you write once", body: "Categorise by payee, amount or memo. Rules show how many past transactions they would have caught before you save them.", stageTitle: "halden.app/rules", stage: ( <div className="grid gap-3 text-[13px]"> {[["Payee contains “AWS”", "Software · 312 past matches", true], ["Amount over $10,000", "Needs approval · 8 past matches", true], ["Memo has “refund”", "Refunds · 44 past matches", false]].map(([a, b, on]) => ( <div key={a as string} className="flex items-center justify-between gap-3 rounded-control border border-border px-3 py-2.5"> <span className="grid"><span className="font-medium">{a}</span><span className="text-muted-foreground">{b}</span></span> <Switch aria-label={`Rule ${a}`} defaultChecked={on as boolean} /> </div> ))} </div> ), }, { title: "Approvals with the receipt attached", body: "Large payments wait for the right person. They see the receipt, the rule that flagged it and the vendor's history.", stageTitle: "halden.app/approvals", stage: ( <div className="grid gap-3 text-[13px]"> <div className="flex items-center justify-between"><span className="font-medium">Ostra Freight · invoice 2209</span><Badge variant="warning">Needs approval</Badge></div> <p className="text-muted-foreground">$18,240.00 · flagged by “Amount over $10,000” · paid 11 times before, always on time.</p> <div className="flex gap-2"><Button size="sm">Approve</Button><Button size="sm" variant="secondary">Ask a question</Button></div> </div> ), }, { title: "A close you can read", body: "One page shows what changed since last month and why, ready to send to the board.", stageTitle: "halden.app/close/september", stage: ( <div className="grid gap-2 text-[13px]"> <div className="flex items-baseline justify-between"><span className="text-muted-foreground">Cash</span><span className="text-xl font-medium tabular-nums">$1.24M</span></div> <Sparkline label="Cash over 12 months" values={[0.82, 0.88, 0.85, 0.94, 0.99, 1.02, 0.98, 1.08, 1.12, 1.15, 1.19, 1.24]} /> <p className="text-muted-foreground">Up 4% on August, mostly two annual renewals paid early.</p> </div> ), }, ]} /> ); }
API reference #
FeatureTabsBlock
component
Accessibility #
It exposes tablist, tab and tabpanel semantics, with aria-orientation, aria-label, aria-selected, aria-controls, aria-hidden and aria-pressed kept in step with state.
Motion #
It checks prefers-reduced-motion itself and settles immediately instead of travelling. Every state is still reachable.