# goodui > Animated components and application blocks for shadcn/ui: scheduling and booking, dashboards, tables, billing, auth, onboarding, notifications and AI chat. React 19, Tailwind v4, works with Base UI and Radix shadcn. Twelve style packs (clean, brutal, glass, retro98, pixel, terminal, editorial, soft, neon, paper, swiss, luxe) via data-style on ; goodui Native has the same looks. Every goodui component with its full docs and API. Index: https://goodui.dev/llms.txt # Number flow > Digits roll to the new value with locale formatting, prefixes and suffixes. Animated rolling number for React and shadcn: digits roll to the new value Page: https://goodui.dev/components/numbers/number-flow ## Install ```bash npx shadcn@latest add @goodui/number-flow ``` Without the namespace: `npx shadcn@latest add https://goodui.dev/r/number-flow.json`. The file lands in `components/goodui/number-flow.tsx`. ## When to use - A live value that changes while the user watches: revenue on a dashboard, a cart total, seats left. - Prices that switch with a monthly/yearly toggle, so the change is visible instead of a jump. - Small counts next to controls (+ / - steppers) where each change should feel physical. ## When not to use - A number that should count up once when it scrolls into view: use counter-up. - Showing the change itself (+12.4%) rather than the value: use delta-badge. - Values in a dense table that update often: use cell-flash, which signals the direction without moving the layout. ## Usage ```tsx import { NumberFlow } from "@/components/goodui/number-flow" export function PlanPrice({ yearly }: { yearly: boolean }) { return (
per seat / month
) } ``` ### Dashboard revenue in another locale ```tsx ``` ### Percent with prefix and suffix ```tsx ``` ## API ### NumberFlow | Prop | Type | Default | Description | | --- | --- | --- | --- | | value (required) | `number` | — | Number to show. Changing it rolls each digit to its new value. | | format | `Intl.NumberFormatOptions` | — | Intl.NumberFormat options, e.g. { style: "currency", currency: "USD" }. | | locale | `string` | — | Locale passed to Intl.NumberFormat ("de-DE" gives 1.234,5). Defaults to the GooduiLocaleProvider language, else en-US. | | prefix | `string` | `""` | Text placed before the formatted number, e.g. "~". | | suffix | `string` | `""` | Text placed after the formatted number, e.g. " / mo". | | className | `string` | — | Classes merged onto the root element. | ## Accessibility - The formatted value is rendered once in an sr-only span; the rolling digit columns are aria-hidden, so screen readers never read ten digits per column. - There is no aria-live region: wrap it in your own live region if a change must be announced. ## Motion - Each digit is a 0-9 column that slides on a spring (stiffness 140, damping 20, mass 0.8). A 9 going to 0 rolls back down the column through every digit, it does not wrap. - Separators, currency symbols, prefix and suffix swap instantly; only digits roll. - Characters are keyed by their distance from the end, so units stay in their column when the number gains or loses digits. - Reduced motion: the transition becomes duration 0, so digits snap to the new value. ## Performance - Each digit is one motion span animating transform only. There are no timers; nothing runs while the value is stable. ## Notes for AI - Pass a number to value and formatting through format (Intl.NumberFormatOptions) and locale; do not pre-format it into a string. - For currency use format={{ style: "currency", currency: "USD" }}, not prefix="$". - It animates on value change only; it does not count up on mount. Use counter-up for that. - Give the font size and weight through className; the component sets tabular-nums itself. - There is no labels prop: the only text it renders is the formatted value. ## FAQ **Does it animate on first render?** No. initial is false, so it shows the value immediately and only rolls when value changes. Use counter-up for a count from zero. **Can it show decimals and currencies?** Yes. Anything Intl.NumberFormat formats works: currency, percent, compact notation, fixed fraction digits, any locale. Each digit in the output rolls. ## Related - counter-up: https://goodui.dev/components/numbers/counter-up - delta-badge: https://goodui.dev/components/numbers/delta-badge - cell-flash: https://goodui.dev/components/numbers/cell-flash - stat-card: https://goodui.dev/components/core/stat-card Last updated: 2026-11-02 --- # Shimmer button > A light sweep runs across the button. Pure CSS; the sweep is removed for reduced motion. Shimmer button for React and shadcn: CSS light sweep across a CTA Page: https://goodui.dev/components/buttons/shimmer-button ## Install ```bash npx shadcn@latest add @goodui/shimmer-button ``` Without the namespace: `npx shadcn@latest add https://goodui.dev/r/shimmer-button.json`. The file lands in `components/goodui/shimmer-button.tsx`. ## When to use - One call to action that should catch the eye on a landing or pricing page ("Start free trial"). - An upgrade prompt inside an app, where a slow sweep hints at the action without a badge. - When you want a single standalone file with no variants to learn: one primary-colored button with a sweep. ## When not to use - You already use the Core button: button with animation="shimmer" gives the same sweep plus every variant, tone, size and the loading state. That is the simpler choice in most apps. - Several buttons on one screen: a looping sweep on each one competes for attention; keep shimmer to the main action and use button for the rest. - Async actions that need loading, success and error feedback: use stateful-button. ## Pick by what matters | Need | Use | | --- | --- | | Calmer, occasional sweep | duration={4} | | Faster, more energetic sweep | duration={1.6} | | Band visible on a light or custom background | shimmerColor="rgb(255 255 255 / 0.7)" or a brand color, plus your own className for the face | | Same sweep with variants, tones and loading | button with animation="shimmer" | ## Usage ```tsx import { Sparkles } from "lucide-react" import { ShimmerButton } from "@/components/goodui/shimmer-button" export function UpgradeCta({ onUpgrade }: { onUpgrade: () => void }) { return ( Upgrade to Pro ) } ``` ### Large hero button with a slower sweep ```tsx Book your first cleaning ``` ### Custom face and band color ```tsx Accept quote · $240 ``` ## API ### ShimmerButton | Prop | Type | Default | Description | | --- | --- | --- | --- | | duration | `number` | `2.4` | Seconds per sweep. | | shimmerColor | `string` | — | Color of the light band. | Also accepts every ) } ``` ### Vertical setup checklist with inline actions ```tsx Connect }, { title: "Add your services", description: "3 services, 30–120 min", content: }, { title: "Share your link", description: "book.studio.co/lena", content: }, ]} /> ``` ### Payment step failed ```tsx ``` ## API ### Stepper | Prop | Type | Default | Description | | --- | --- | --- | --- | | steps (required) | `StepperStep[]` | — | The steps, in order. | | value | `number` | — | Index of the current step. Use `steps.length` to show every step as complete. | | defaultValue | `number` | `0` | Starting step when uncontrolled. | | onValueChange | `(index: number) => void` | — | Called with the step index when a clickable step is clicked. | | orientation | `"horizontal" \| "vertical"` | `"horizontal"` | horizontal: steps in a row with titles under the circles. vertical: a column that can open content under the current step. | | clickable | `"completed" \| "all" \| "none"` | `"completed"` | Which steps can be clicked to jump to them. | | labels | `Partial` | — | Text for built-in controls and screen readers; pass translations here. | Also accepts every
    attribute. ## Keyboard | Keys | Action | | --- | --- | | Tab | Moves between clickable steps (they are native buttons); other steps are not focusable. | | Enter / Space | Jumps to the focused clickable step. | ## Accessibility - Renders an
      of
    1. items; the current one has aria-current="step". Pass aria-label on the stepper to name it. - Each title is followed by an sr-only status ("completed", "current step", "not started", "needs attention"), translatable through labels. - Clickable steps are real ) } ``` ### Focus a specific control when it opens ```tsx ``` ### Slower, bouncier morph and a translated close button ```tsx {/* MorphingDialogTrigger as above */} {/* media, title, subtitle, description */} ``` ## API ### MorphingDialog | Prop | Type | Default | Description | | --- | --- | --- | --- | | children (required) | `React.ReactNode` | — | Content. | | open | `boolean` | — | Controlled open state. Pair with onOpenChange. | | defaultOpen | `boolean` | `false` | Initial open state when uncontrolled. | | onOpenChange | `(open: boolean) => void` | — | Called with the next state when the trigger, Esc, the backdrop or a close control opens or closes it. | | transition | `Transition` | — | Transition for the shared-layout morph. Reduced motion always snaps. | ### MorphingDialogTrigger Also accepts every attribute. ### MorphingDialogContainer | Prop | Type | Default | Description | | --- | --- | --- | --- | | children (required) | `React.ReactNode` | — | Content. | | className | `string` | — | Classes for the fixed layer that centers the dialog (padding, alignment). | | backdropClassName | `string` | — | Classes for the backdrop. | ### MorphingDialogContent Also accepts every attribute. ### MorphingDialogMedia Also accepts every attribute. ### MorphingDialogTitle | Prop | Type | Default | Description | | --- | --- | --- | --- | | children (required) | `React.ReactNode` | — | Content. | | className | `string` | — | Classes merged onto the root element. | ### MorphingDialogSubtitle | Prop | Type | Default | Description | | --- | --- | --- | --- | | children (required) | `React.ReactNode` | — | Content. | | className | `string` | — | Classes merged onto the root element. | ### MorphingDialogDescription Also accepts every attribute. ### MorphingDialogClose | Prop | Type | Default | Description | | --- | --- | --- | --- | | labels | `Partial` | — | Text for built-in controls and screen readers; pass translations here. | Also accepts every } >
      • 2 h 30 min with Maya Chen
      • Deposit paid: $40 of $185
      • Note: prefers a cooler tone than last time.
      ) } ``` ### Side form drawer with a footer submit ```tsx Save client} >
      ``` ### Navigation drawer from the left ```tsx ``` ## API ### DrawerSpring | Prop | Type | Default | Description | | --- | --- | --- | --- | | title (required) | `React.ReactNode` | — | Heading of the drawer; it is also the dialog's accessible name. | | open | `boolean` | — | Controlled open state. Pair with onOpenChange. | | defaultOpen | `boolean` | `false` | Initial open state when uncontrolled. | | onOpenChange | `(open: boolean) => void` | — | Called with false on Esc, the close button, a backdrop click or a drag past the lowest point. | | side | `"bottom" \| "right" \| "left"` | `"bottom"` | "bottom" is a sheet with optional snap points; "right" and "left" are full-height side drawers. | | snapPoints | `number[]` | — | Bottom sheet only: visible fractions of the sheet height, e.g. [0.5, 1] for half and full. | | defaultSnap | `number` | `0` | Snap point index used when the sheet opens. | | onSnapChange | `(index: number) => void` | — | Called with the index into the sorted snapPoints after a drag or a handle press moves the sheet. | | description | `React.ReactNode` | — | Line under the title, linked to the dialog with aria-describedby. | | footer | `React.ReactNode` | — | Sticky area under the scrolling body, for actions. | | dismissible | `boolean` | `true` | When true (default), Esc, the close button, a backdrop click and dragging past the lowest point close the drawer. When false, none of them do and the close button is hidden: close it from your own controls (e.g. the footer). | | className | `string` | — | Classes for the panel (width, height, radius). | | bodyClassName | `string` | — | Classes for the scrolling body that wraps children. | | children | `React.ReactNode` | — | Content. | | labels | `Partial` | — | Text for built-in controls and screen readers; pass translations here. | ## Keyboard | Keys | Action | | --- | --- | | Esc | Closes the drawer unless dismissible is false; focus returns to the element that had it before opening. | | Tab / Shift+Tab | Cycles through focusable elements inside the drawer; focus cannot leave it while open. | | Enter / Space | On the grab handle of a bottom sheet with two or more snapPoints: moves to the next snap point, wrapping back to the lowest. | ## Accessibility - The panel has role="dialog" and aria-modal="true", named by title and described by description when you pass one. - On open, focus goes to the first [data-autofocus] element, else the first focusable element after the grab handle (usually the close button). Add data-autofocus to a field if typing comes first. - With snap points, the handle is a button labelled labels.expand or labels.collapse ("Expand sheet" / "Collapse sheet"); without them it is hidden from assistive tech and the tab order. - The close button is labelled labels.close (default "Close") and is hidden when dismissible is false. Page scroll is locked while open. ## Motion - Open, close and snap use a spring (stiffness 380, damping 38, mass 0.9) that starts with the release velocity of the drag; closing uses a stiffer spring (420 / 42). - Dragging the header (handle, title, close row) moves the panel 1:1, with a rubber-band resistance past fully open. The body scrolls normally. - On release the panel goes to the snap point nearest to where the throw would land (position + velocity × 0.2 s); a flick faster than 700 px/s always moves at least one step. With dismissible, closed is one of the targets. - The backdrop opacity follows the panel between the lowest snap point and closed, so it fades while you drag down. - Reduced motion: open, close and snap jump instantly (duration 0); dragging still follows the pointer. ## Performance - Dragging updates a motion value, so the panel and backdrop move without React re-renders until release. ## Notes for AI - title is required; it is the dialog's accessible name. Use description for one line of context. - snapPoints only apply to side="bottom"; they are fractions of the sheet height (0 to 1) and are sorted ascending, so onSnapChange indexes refer to the sorted list. - Put actions in footer, not at the end of children, so they stay visible while the body scrolls. - It renders position: fixed without a portal: do not place it inside elements with transform or filter. - dismissible={false} disables Esc, backdrop click and drag-to-close and hides the close button, so always give the user an explicit way out (e.g. Cancel / Save in footer). ## FAQ **Does onSnapChange fire when the sheet opens?** No. It fires after a drag release or a handle press moves the sheet. The opening snap point is defaultSnap. **Can I drag it from the body?** No. Only the header area drags, so scrolling content inside the body never fights the gesture. ## Related - morphing-dialog: https://goodui.dev/components/overlays/morphing-dialog - popover-morph: https://goodui.dev/components/overlays/popover-morph - dropdown-stagger: https://goodui.dev/components/overlays/dropdown-stagger - bottom-nav: https://goodui.dev/components/navigation/bottom-nav Last updated: 2026-11-02 --- # Popover morph > A small button such as "Add note" morphs into a floating form panel, its label sliding into the header, and shrinks back into the button on save, Esc or an outside click. Popover that morphs from a button into a floating form, for React and shadcn Page: https://goodui.dev/components/overlays/popover-morph ## Install ```bash npx shadcn@latest add @goodui/popover-morph ``` Without the namespace: `npx shadcn@latest add https://goodui.dev/r/popover-morph.json`. The file lands in `components/goodui/popover-morph.tsx`. ## When to use - Quick input tied to one button: add a note to a booking, set a reminder, apply a discount code. - Toolbars and record headers where a full dialog would interrupt the page. - Small settings that should close as soon as the user clicks elsewhere. ## When not to use - A list of actions or options: use dropdown-stagger, which has menu roles and arrow-key navigation. - Read-only hints on hover: use tooltip-animated. - Larger forms or anything that must not be lost on an outside click: use drawer-spring or morphing-dialog, which are modal. ## Pick by what matters | Need | Use | | --- | --- | | Trigger near the top of the page | side="bottom" (default): the panel grows down from the trigger | | Trigger near the bottom (composer, footer bar) | side="top": the panel grows up | | Trigger at the right end of a toolbar | align="end" | | Narrow panel for one field | width={280} | ## Usage ```tsx import * as React from "react" import { StickyNote } from "lucide-react" import { Button } from "@/components/ui/button" import { Textarea } from "@/components/ui/textarea" import { PopoverMorph } from "@/components/goodui/popover-morph" export function AddNote({ onAdd }: { onAdd: (text: string) => void }) { const [draft, setDraft] = React.useState("") return ( Add note} title={<> Note for Sofia}> {({ close }) => (
      { e.preventDefault() if (!draft.trim()) return onAdd(draft.trim()) setDraft("") close() }} className="space-y-2.5" >