Trial banner

Pro

A 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.

Live · keyboard ready
9 days left in your Pro trialKeep Okta SSO and 12 seats after Nov 5.
  • 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.

example.tsx
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.

PropTypeDefaultDescription
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.