Skip to content
txbrendan/ui

Announcement Dialog

beta

A launch popup with feature tiles and one button, plus an optional review step that takes people from the news to checkout.

Built from Agreement, Avatar, Button, Dialog, and Switch.

Preview

Introducing Plus

One plan for the whole team: Schedule, Chat, and Stats.

Everything in one plan
  • 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

Step 2 of 2

Review your new plan

Agree to each term to go on to checkout.

jordan@example.com

This is how you sign in.

One account for all three apps

We set up Schedule, Chat, and Stats with the account details you already gave us.

Linking is permanent

After checkout, the linked apps cannot be split apart again.

Extras are billed on top

Extra storage and SMS reminders are charged by use, and are not in the monthly price.

Plus$12/monthplus any tax where you live

Plus

$12/month

plus any tax where you live

  • One sign-in for all three apps

    The same login works in Schedule, Chat, and Stats.

  • Your data stays with you

    Your teams, chats, and results all move to the new plan.

  • More included use

    Limits are shared across the apps, and refresh each week.

  • Extras are billed by use

    Extra storage and SMS reminders cost more, at the listed rates.

Plus

jordan@example.com

Member since Oct 2026

Everything in one plan

news with no checkout: the button links on

narrow container

Introducing Plus

One plan for the whole team: Schedule, Chat, and Stats.

Everything in one plan
  • 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. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the website component. It also adds every component it uses. Its own files go in your components folder, 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

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"
/>