Announcement Dialog
betaA launch popup with feature tiles and one button, plus an optional review step that takes people from the news to checkout.
Preview
Introducing Plus
One plan for the whole team: Schedule, Chat, and Stats.
Schedule, with who's coming
Chat and voice notes, by team
Stats and trends, all year
Generous limits, shared across all three apps.See what Plus adds
Recommended: a trigger people can come back to, and a review step before checkout
On your site, add showOnce="plus-launch" so it also opens once for each visitor.
the review step on its own page
Review your new plan
Agree to each term to go on to checkout.
jordan@example.com
One account for all three apps
Linking is permanent
Extras are billed on top
Plus$12/month
Plus
$12/month
One sign-in for all three apps
Your data stays with you
More included use
Extras are billed by use
Plus
jordan@example.com
Member since Oct 2026
news with no checkout: the button links on
narrow container
Introducing Plus
One plan for the whole team: Schedule, Chat, and Stats.
Schedule, with who's coming
Chat and voice notes, by team
Stats and trends, all year
Generous limits, shared across all three apps.See what Plus adds
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the website component. It also adds every component it uses. Its own files go in your
componentsfolder, and they are yours to edit.npx shadcn@latest add @txbrendan/announcement-dialog
Use it for
- Recommended: three features, each with a `visual` drawn from parts and a short `title`. Three fills the row on wide screens and stacks into rows on phones.
- Recommended: pass a `trigger`, like a What's new button, so people can open it again after they close it.
- Recommended for a launch: add `showOnce` with a key named for this launch, like "plus-launch". It opens once per browser and not again after it closes. Use a new key for the next launch.
- Recommended when the news leads to a purchase: pass `review={<AnnouncementReview />}`. The main button moves to it, and its Back button comes back.
- Recommended: leave `animated` on. Tiles lift on hover, and reduced motion keeps them still.
- News with no checkout: give `action` an `href`, and the button links on. Pass `className="max-w-lg"` for one feature.
- AnnouncementReview on its own page: pass `onBack` to show Back. Send people to checkout in `onProceed`, which runs once every required term is agreed.
- Pass your router's link through `renderLink`, like next/link, for the note's link, the button's href, and the review's links.
- AnnouncementIntro alone draws a still picture of the popup, for a page or a preview.
- The headline is set in the theme's `font-headline`. Load Geist Serif as `--font-geist-serif`, like `geist/font/serif/variable` in Next.js, or it falls back to the system serif.
Avoid it for
- Opening it on every visit. Use `showOnce`, or open it yourself only when there is news.
- Showing it before people have seen the page, like on a first visit from an ad. Let them land first, and offer a trigger.
- More than four features. Link to a page that tells more.
- Checkboxes for terms. Each term is an Agreement, an "I agree" button that records who and when, so nothing is ticked by accident.
- Starting email updates on. The switch starts off, so nobody is signed up without choosing it.
- Hiding what costs extra. Put charges on top of the price last in the plan's `perks`, and as a term to agree to.
- A small yes or no question. Use Alert Dialog.
- A banner that stays on the page. Use Alert or Notice.
Props
- title
Required. What is new, like "Introducing Plus". It names the dialog.
ReactNode
- description
One line under the title that says what it means for people.
ReactNode
- label
A short label between two lines over the features, like "Everything in one plan".
ReactNode
- features
Two to four. Recommended: three. Each visual fills a 4:3 tile. A badge is a sticker on the tile, shown when the tiles sit in a row, from 36rem wide.
{ title: ReactNode; visual?: ReactNode; badge?: ReactNode }[]
- note
One line in a soft box under the features, like a limit or a promise.
ReactNode
- link
A link after the note, like See the guide.
{ label: ReactNode; href: string }
- action
The one main button. With a review step it moves to it. With no review and no href, it closes the dialog.
{ label?: ReactNode; href?: string; onClick?: () => void } · default { label: "Try it" }
- animated
Recommended: true. Feature tiles lift on hover. Reduced motion keeps them still.
boolean · default true
- renderLink
Draws links. Spread the props onto your router's link: (props) => <Link {...props} />.
(props: AnnouncementLinkProps) => ReactNode · default a plain <a>
- trigger
Recommended. The button that opens it, like <Button emphasis="outline">What's new</Button>.
ReactElement
- showOnce
Recommended for a launch. A storage key. It opens by itself once per browser, after the page loads, and not again after it closes. When storage is blocked it never opens by itself.
string
- open
Open it yourself. Use it with onOpenChange.
boolean
- defaultOpen
Open on first render, when you do not control it.
boolean · default false
- onOpenChange
Called when it opens or closes.
(open: boolean) => void
- review
Recommended when the news leads to a purchase. A second step, like <AnnouncementReview />.
ReactNode
- container
Where the dialog opens. By default, at the end of the page body.
HTMLElement | RefObject<HTMLElement>
- className
On AnnouncementDialog, classes for the popup, like max-w-lg. On AnnouncementIntro, classes for its root.
string
- AnnouncementIntro
Part. What the popup says, with no dialog: a still picture for a page or a preview. Takes the props from title to animated.
AnnouncementIntroProps
- AnnouncementReview
Part. The last step before checkout: account, terms, an email switch, and the plan as a card that turns over. Its props follow.
AnnouncementReviewProps
- account
AnnouncementReview. Required. Who is signing up. Their name goes on each agreement's record.
{ name: string; email: string; photo?: string; note?: ReactNode }
- plan
AnnouncementReview. Required. { name, price?, period?, note?, perks?, memberSince?, tagline?, marks? }. The front shows the price and perks. The back is a member card.
AnnouncementPlan
- terms
AnnouncementReview. Terms to agree to, each with an "I agree" button. Required by default. Recommended: three or fewer.
{ id: string; title: ReactNode; text?: ReactNode; required?: boolean }[]
- updates
AnnouncementReview. A switch for email updates. It always starts off. Pass false to leave it out.
ReactNode | false · default "Email me about new features."
- step
AnnouncementReview. Where they are, like "Step 2 of 2".
ReactNode
- links
AnnouncementReview. Links under the card, like See the guide and Get help.
{ label: ReactNode; href: string }[]
- onBack
AnnouncementReview. Shows Back. Inside AnnouncementDialog, Back returns to the announcement by itself.
() => void
- onProceed
AnnouncementReview. Runs once every required term is agreed. Before that, a press shows an error on each one left.
(result: { agreed: Record<string, AgreementRecord>; updates: boolean }) => void
- proceedLabel
AnnouncementReview. The main button's words.
ReactNode · default "Proceed to checkout"
- termError
AnnouncementReview. Under each required term left unagreed.
ReactNode · default "Agree to this to go on."
- loading
AnnouncementReview. The proceed button shows a spinner, and the terms and switch are blocked.
boolean · default false
| Prop | Type | Default | What it does |
|---|---|---|---|
| title | ReactNode | - | Required. What is new, like "Introducing Plus". It names the dialog. |
| description | ReactNode | - | One line under the title that says what it means for people. |
| label | ReactNode | - | A short label between two lines over the features, like "Everything in one plan". |
| features | { title: ReactNode; visual?: ReactNode; badge?: ReactNode }[] | - | Two to four. Recommended: three. Each visual fills a 4:3 tile. A badge is a sticker on the tile, shown when the tiles sit in a row, from 36rem wide. |
| note | ReactNode | - | One line in a soft box under the features, like a limit or a promise. |
| link | { label: ReactNode; href: string } | - | A link after the note, like See the guide. |
| action | { label?: ReactNode; href?: string; onClick?: () => void } | { label: "Try it" } | The one main button. With a review step it moves to it. With no review and no href, it closes the dialog. |
| animated | boolean | true | Recommended: true. Feature tiles lift on hover. Reduced motion keeps them still. |
| renderLink | (props: AnnouncementLinkProps) => ReactNode | a plain <a> | Draws links. Spread the props onto your router's link: (props) => <Link {...props} />. |
| trigger | ReactElement | - | Recommended. The button that opens it, like <Button emphasis="outline">What's new</Button>. |
| showOnce | string | - | Recommended for a launch. A storage key. It opens by itself once per browser, after the page loads, and not again after it closes. When storage is blocked it never opens by itself. |
| open | boolean | - | Open it yourself. Use it with onOpenChange. |
| defaultOpen | boolean | false | Open on first render, when you do not control it. |
| onOpenChange | (open: boolean) => void | - | Called when it opens or closes. |
| review | ReactNode | - | Recommended when the news leads to a purchase. A second step, like <AnnouncementReview />. |
| container | HTMLElement | RefObject<HTMLElement> | - | Where the dialog opens. By default, at the end of the page body. |
| className | string | - | On AnnouncementDialog, classes for the popup, like max-w-lg. On AnnouncementIntro, classes for its root. |
| AnnouncementIntro | AnnouncementIntroProps | - | Part. What the popup says, with no dialog: a still picture for a page or a preview. Takes the props from title to animated. |
| AnnouncementReview | AnnouncementReviewProps | - | Part. The last step before checkout: account, terms, an email switch, and the plan as a card that turns over. Its props follow. |
| account | { name: string; email: string; photo?: string; note?: ReactNode } | - | AnnouncementReview. Required. Who is signing up. Their name goes on each agreement's record. |
| plan | AnnouncementPlan | - | AnnouncementReview. Required. { name, price?, period?, note?, perks?, memberSince?, tagline?, marks? }. The front shows the price and perks. The back is a member card. |
| terms | { id: string; title: ReactNode; text?: ReactNode; required?: boolean }[] | - | AnnouncementReview. Terms to agree to, each with an "I agree" button. Required by default. Recommended: three or fewer. |
| updates | ReactNode | false | "Email me about new features." | AnnouncementReview. A switch for email updates. It always starts off. Pass false to leave it out. |
| step | ReactNode | - | AnnouncementReview. Where they are, like "Step 2 of 2". |
| links | { label: ReactNode; href: string }[] | - | AnnouncementReview. Links under the card, like See the guide and Get help. |
| onBack | () => void | - | AnnouncementReview. Shows Back. Inside AnnouncementDialog, Back returns to the announcement by itself. |
| onProceed | (result: { agreed: Record<string, AgreementRecord>; updates: boolean }) => void | - | AnnouncementReview. Runs once every required term is agreed. Before that, a press shows an error on each one left. |
| proceedLabel | ReactNode | "Proceed to checkout" | AnnouncementReview. The main button's words. |
| termError | ReactNode | "Agree to this to go on." | AnnouncementReview. Under each required term left unagreed. |
| loading | boolean | false | AnnouncementReview. The proceed button shows a spinner, and the terms and switch are blocked. |
Accessibility
- The title names the dialog and the description describes it. Focus starts on the Close button, at the top.
- Escape, the Close button, and a press on the backdrop close it. Tab stays inside while it is open.
- Moving to the review step and back puts focus on the new step's title, so it never drops to the page.
- Feature pictures and badges are aria-hidden. The feature's title says what it is.
- With showOnce, it opens after the page loads, never during the first render, and only once.
- Each term is an Agreement: a group named by its title. A press on proceed with terms left moves focus to the first one, whose error is read with it.
- The turn button says what it does and is pressed while the back shows. The hidden face is inert, so screen readers and Tab skip it.
- In forced colors, tiles, the note, and the card's back get borders, so their edges still show.
Examples
Recommended: a launch with a review step
<AnnouncementDialog
title="Introducing Plus"
description="One plan for the whole team: Schedule, Chat, and Stats."
label="Everything in one plan"
features={[
{ title: "Schedule, with who's coming", visual: <ScheduleMock />, badge: <Badge>24 going</Badge> },
{ title: "Chat and voice notes, by team", visual: <ChatMock /> },
{ title: "Stats and trends, all year", visual: <StatsMock /> },
]}
note="Generous limits, shared across all three apps."
link={{ label: "See the guide", href: "/plus" }}
trigger={<Button emphasis="outline">What's new</Button>}
showOnce="plus-launch"
review={
<AnnouncementReview
step="Step 2 of 2"
account={{ name: user.name, email: user.email }}
terms={[{ id: "permanent", title: "Linking is permanent", text: "The linked apps cannot be split apart again." }]}
plan={{ name: "Plus", price: "$12", period: "/month", perks: [{ title: "One sign-in for all three apps" }] }}
onProceed={({ updates }) => startCheckout({ updates })}
/>
}
/>News with no checkout
<AnnouncementDialog
title="Voice notes are here"
description="Hold the mic in any team chat to send one."
features={[{ title: "Chat and voice notes, by team", visual: <ChatMock /> }]}
action={{ label: "Try it now", href: "/chat" }}
showOnce="voice-notes"
className="max-w-lg"
/>