Trial banner
ProA trial status banner where a ring counts down the days left, the copy names what the workspace loses, and the upgrade path is one click away. It comes as a slim banner, a sidebar card or a header pill. It shifts to a warning tone in the last days, has expired and upgraded states, and folds into the pill when dismissed or snoozed.
- A SaaS app with a free trial that needs a quiet, always visible upgrade path.
- Telling a workspace what it loses when the trial ends and nudging harder in the last days.
- Showing an expired trial without blocking the app.
- A hard paywall that blocks a feature; use a modal or an upgrade screen instead.
- Generic announcements or release notes; use a plain banner or toast.
- Usage limits (credits, seats); use a usage meter.
Installation
Pro source and install commands unlock with a Pro plan.
import { TrialBanner } from "@/components/arc/trial-banner/trial-banner"; export function AppTrialBanner({ trial }: { trial: { daysLeft: number; endsAt: string } }) { return ( <TrialBanner daysLeft={trial.daysLeft} endsAt={trial.endsAt} onAddPayment={async signal => { const res = await fetch("/api/billing/checkout", { method: "POST", signal }); if (!res.ok) throw new Error("Checkout failed"); window.location.assign((await res.json()).url); }} onComparePlans={() => router.push("/pricing")} onSnooze={({ until }) => saveSnooze(until)} onDismiss={() => saveDismissed()} /> );}TrialBanner
A trial status banner with a ring countdown, what the customer loses, and the upgrade path. Three variants share one state model: active, warning (the last days), expired and upgraded. Dismiss and snooze collapse it into the trial pill.
variant"banner" | "card" | "pill""banner"banner: a slim row for the top of an app. card: a sidebar card with the feature list. pill: a compact header pill that opens the card in a popover.planstring"Pro"The plan being trialed, used in every line of copy.daysLeftnumber9Whole days left. 0 or less shows the expired state.trialDaysnumber14Trial length in days. The ring shows daysLeft / trialDays.endsAtDate | number | string–When the trial ends, for the date copy. Defaults to October 27, 2026 plus daysLeft, a sample date; pass your real end date.warnAtnumber3The last N days switch to the warning tone, with copy that names the end day.workspacestring"Northwind"The workspace name, used in the expired card copy.featuresTrialFeature[]Okta SSO, Slack alerts, Linear sync, team seatsWhat the customer loses without paying, listed in the card: { id, label, detail?, icon? }. Icons are 14px marks.lossSummarystring"Okta SSO and 12 seats"One short phrase for the banner copy. Keep it short so the slim banner stays on one line.onAddPayment(signal: AbortSignal) => Promise<void> | void–Opens checkout or the billing portal. Resolve once a payment method is on file to show the upgraded state; throw to show the error state with Try again. Simulated (900ms) when left out.onComparePlans() => void–Opens your plans page. Without it the banner shows a short simulated note.onManageBilling() => void–The secondary action in the upgraded state.onDismiss() => void–Called when the person dismisses the banner. Persist it.onSnooze(snooze: TrialSnooze & { until: Date }) => void–Called with the chosen snooze (tomorrow, in 3 days, or when the warning starts). Persist `until` and stop rendering until then.collapseTo"pill" | "none""pill"After dismiss or snooze: collapse to the trial pill, which reopens the banner, or render nothing.dismissiblebooleantrueShows the dismiss button.snoozablebooleantrueShows the snooze menu.classNamestring–Class on the root.- Tab
- Moves through Compare plans, Add payment method, Snooze and Dismiss.
- EnterorSpace
- Runs the focused action, opens the snooze menu or opens the pill's card.
- Arrow upordown
- Moves through snooze options; Escape closes the menu or the pill's popover.
- The banner and card are labelled regions ("Pro trial"); the ring is decorative and the days are always in the text.
- A polite live region announces billing, snooze and dismiss results, and says when an action was simulated.
- Status never relies on color: the warning tone also changes the copy to name the end day, expired shows a lock, upgraded a check.
- After dismiss or snooze, focus moves to the trial pill; reopening it moves focus to the primary action.
- The ring arc springs to its new length with no bounce (0.24s); its color follows the tone in 160ms.
- Copy, days and icons swap with one crossfade (old out in 100ms, new in over 160ms with a 2px rise) in a shared grid cell, so nothing shifts.
- Button labels morph their width without bounce; the pill label width springs to the measured text.
- Reduced motion keeps every end state and drops the crossfades and springs.
- The banner uses a container query: under 600px the buttons move under the copy, aligned with its left edge, and the sub line may take two lines (space reserved).
- The card fills its container up to 340px. The pill's popover keeps 12px from the viewport edge.
- Holds up at 360px, 390px and desktop widths without horizontal scroll.
- No timers or loops while idle; only the simulated billing call and the short simulated note use a timeout.
- Marks are inline SVG; no images or fonts are loaded.
Notes for AI
Give your coding assistant the Markdown reference instead of screenshots.
- Pass daysLeft and endsAt from your billing provider (for example Stripe's subscription trial_end); the component never reads the clock itself, so it renders the same on server and client.
- Use variant="banner" above the app content, "card" at the bottom of a sidebar, "pill" in a dense header.
- Keep lossSummary to a few words and features to three or four rows; these are the reasons to pay, not a feature table.
- Persist onDismiss and onSnooze yourself, then stop rendering the component until the snooze ends.
The full library index for assistants is at /llms.txt.