shadcn.io is not affiliated with official shadcn/ui
Upgrade Plan Dialog Block for React Aria
A product-analytics usage page with its upgrade dialog shown open: your plan and the next joined by an arrow, a yearly switch, and a table of limits with what you have now and what you would have after.
Installation
Overview
Show people what they get before they upgrade, with this upgrade dialog for React with React Aria. It opens from the Usage page of Gaugefield, a product-analytics tool: the page's header holds an outline Upgrade Button, and the page itself, a rail, three KPI cells one of which is a project meter nearly full, and a bar chart, is drawn under a soft scrim inside a hairline frame with a plus mark on each corner. The Dialog is a diff. Your plan, Team, sits muted at the start, an arrow joins it to Scale at the end, raised and marked with a Badge, Recommended. A Switch moves the Scale price between monthly and yearly. Under them a table lists the limits with a Now column and an After column: seats, projects, history and support, each changed limit carrying an arrow and the word more or new. Upgrade to Scale is the only Shine Button; upgrading changes the meter behind the dialog from 18 of 20 to 18 of Unlimited and turns the header's button into Start over. For preview the dialog is drawn open in place when the page loads; the Upgrade button opens the real dialog. For limits, paywalls and any product with plans that differ by numbers.
FAQ
Why is the dialog already open, and what do I keep?
So you can see it without a click. On load the dialog is a copy drawn in place over the page: a native dialog element that isn't modal, so nothing is trapped, hidden or focused, and the page around the preview keeps working. The Upgrade button opens the real Dialog, modal, with its focus trap, Escape and focus return, and the copy stays closed until a reload. Both show the same UpgradeFields component and share the block's state. In your app keep the Dialog and drop the copy: the "in-place" state, the scrim and the dialog element inside the frame.
How do I change the limits?
Edit LIMITS: each has a label, the before text for the plan you are on, the after text for the next one, and a tag, the word shown beside a changed limit (more or new). The table has three columns on every screen, with the tag moving under the value on a phone, and each value is an isolated run of text so it reads right in a right-to-left page. The arrow in an After cell mirrors there. Nothing here is a struck-through price: the dialog shows the new limits, not a discount.
What does the switch change?
Only the Scale head: $119 a month billed monthly, or $99 a month billed yearly, with a line saying what yearly saves. Your own plan's price stays as it is. What an upgrade costs part-way through a billing period (proration) isn't shown here, because that belongs on a billing page: ask your provider for an invoice preview and show it in the same dialog if you need it.
What stays local?
Everything. Upgrading changes the meter and the sentence behind the dialog and says, in the polite status, that nothing was upgraded or charged. The header's button becomes Start over, which puts the page back on Team to try the other cycle. In your app send the plan and the cycle to your server, wait for its answer, and update the meter from what the plan really allows.
Is it accessible from the keyboard?
Yes. The real dialog is named by its title and described by its sentence, focus starts on the yearly switch and stays inside until it closes, and Escape, Not now and the close button return focus to Upgrade. The table has a caption, column headers and row headers, and the switch is named by aria-label. The Upgrade button and the dialog sit inside DialogTrigger, controlled with isOpen and onOpenChange, and the title is a level 2 heading.
How does it look on a phone?
The copy shown on load docks to the foot of the page as a bottom sheet with its buttons pinned, and the two plan heads stay side by side with the arrow between them. The table keeps its three columns at 320 pixels, which is why the tags sit under the values. The real dialog keeps the Dialog's own phone layout, a sheet stuck to the bottom of the screen.