
Pegas city bike
$420
Brooklyn · 214 views
Overlays
Dialog
v1.0.0Added in 1.0.0
A React modal dialog for focused tasks: a spring-scaled card on desktop and a bottom sheet on phones, with nesting and scrolling.
Click the trigger and the page dims while the card scales up from 96% on a smooth spring; the icon tile pops in a beat later. Focus moves to the first field, Tab stays inside, and Escape, a click on the dim page or the round × close it, handing focus back to the trigger. A long body scrolls between the pinned header and footer, and a hairline fades in under the header once it scrolls and above the footer while there is more below; with scrollBehavior outside the whole card scrolls with the page instead. A dialog opened from inside another one stacks on top while the first steps back and up, and closing it brings the first forward again. When it is not dismissible, a click outside or Escape only gives the card a small bump: an action has to answer it. A shortcut, when set, toggles it from anywhere. Below 768px it opens as a bottom sheet you can drag down to close, with the actions stacked full width, 48px tall and the primary on top. With reduced motion it only fades.
Dialog is a free React component in the React overlay components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add dialog, together with Button, Drawer, Motion presets, Tones, useControllableState and 4 more and the npm packages @radix-ui/react-dialog, @radix-ui/react-visually-hidden, lucide-react and motion.
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add dialogImport Dialog from @/components/ui/dialog.
import { Pencil } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Dialog, DialogClose } from "@/components/ui/dialog"
export function EditListing({ saving, onSave }: { saving: boolean; onSave: () => void }) {
return (
<Dialog
trigger={
<Button variant="soft" leftIcon={<Pencil />}>
Edit
</Button>
}
title="Edit listing"
description="Changes show up for buyers right away."
icon={<Pencil />}
closeLabel="Close dialog"
shortcut="mod+e"
footer={
<>
<DialogClose asChild>
<Button variant="soft">Cancel</Button>
</DialogClose>
<Button loading={saving} loadingText="Saving" onClick={onSave}>
Save
</Button>
</>
}
>
<p className="text-sm text-muted-foreground">Title, price, city and description</p>
</Dialog>
)
}defaultOpendefault falsebooleanStarts open (uncontrolled). Pass open and onOpenChange instead to control it.
openbooleanControlled open state. The root keeps it across the 768px breakpoint, so resizing swaps the card for the sheet and keeps it open.
onOpenChange(open: boolean) => voidCalled when it opens or closes: the trigger, Escape, an outside click, a drag on the sheet, a close button, the shortcut.
titledefault ""ReactNodeThe title, also the dialog's accessible name. Always pass one: without it a visually hidden “Dialog” stands in so screen readers still get a name. The demo shows its sample while this is empty.
descriptiondefault ""ReactNodeA quieter line under the title, linked with aria-describedby. Without one the dialog opts out of the description (Radix's aria-describedby={undefined}).
hideTitledefault falsebooleanKeeps the title for screen readers only, for dialogs whose content speaks for itself (a photo, a video).
closeLabeldefault "Close"stringaria-label of the × button, e.g. “Close dialog”. The demo spells it out under the page.
triggerReactNodeThe element that opens it, rendered with asChild: a single element that takes a ref, like a Button. It gets aria-keyshortcuts when a shortcut is set. Leave it out and control open yourself.
iconReactNodeAn icon in a 40px tile before the title (above it when the header is centered), painted in the tone's soft color. It pops in on a bouncy spring as the card lands.
childrenReactNodeThe body. It scrolls on its own between the pinned header and footer (overscroll contained) and drives the hairlines. Forms inside get the first field focused on open.
footerReactNodeThe actions, primary last in the DOM: on the right in a row on desktop, on top when stacked. On the phone sheet they stack full width at 48px whatever size you passed. Wrap Cancel in DialogClose.
tonedefault "neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The icon tile's color: success for a confirmation, danger for a warning about data loss. Needs an icon.
colordefault ""stringAny CSS color for the icon tile. Replaces the tone; the icon is mixed toward the text color so it stays readable. Leave empty to use the tone.
sizedefault "md""sm" | "md" | "lg" | "xl" | "full"Max width 400 / 520 / 720 / 960px, always 16px clear of the screen edges. full fills the screen minus 16px on desktop (a lightbox, a long editor) and becomes a full-height sheet on phones.
radiusdefault "lg""none" | "sm" | "md" | "lg" | "full"Corners 0 / 12 / 16 / 24px (full stays 24px); the icon tile follows. Dialogs default to lg like every large surface. The phone sheet takes 0 / 16 / 24 / 28px.
headerAligndefault "start""start" | "center"start reads like a form: the icon beside the title. center reads like an announcement: the icon above a centered title, and the actions centered too.
showClosedefault truebooleanThe round × in the top corner (ghost, 32px with a 44px hit area on touch). It closes even a dialog that is not dismissible.
scrollBehaviordefault "inside""inside" | "outside"inside: the card stops at 85% of the screen (720px at most), the header and footer stay and the body scrolls. outside: the card grows with its content and the dimmed page scrolls instead, for long reading. The phone sheet and size full always scroll inside.
dismissibledefault truebooleanfalse: outside clicks, Escape, the shortcut and the sheet drag do not close it; the card gives a small bump instead and the sheet loses its handle. The × and actions (DialogClose, or setting open) still close it, e.g. a consent that must be answered.
responsivedefault truebooleanBelow 768px it opens as the drawer's bottom sheet: drag to dismiss, safe-area padding, a 92% height for size full. Off, it stays a centered card on phones too.
shortcutdefault ""stringA key combo in mod+k notation (⌘ on Apple devices, Ctrl elsewhere) that toggles the dialog from anywhere on the page; the trigger gets aria-keyshortcuts. Empty binds nothing.
modaldefault truebooleanfalse: no backdrop, no focus trap and no scroll lock; the page behind stays usable (Radix's modal), and on desktop a click on the page closes it. scrollBehavior outside needs the backdrop, so it scrolls inside then.
onOpenAutoFocus(event: Event) => voidRuns when it opens, before focus moves. Desktop focuses the first focusable element (put the close button last, as the dialog does); the phone sheet focuses itself so no keyboard pops up. Call event.preventDefault() and focus something yourself.
onCloseAutoFocus(event: Event) => voidRuns when it closes, before focus returns to the trigger. Prevent it to send focus elsewhere.
containerHTMLElement | nullPortal target, document.body by default.
classNamesPartial<Record<"overlay" | "content" | "header" | "icon" | "title" | "description" | "body" | "footer" | "close", string>>Extra classes for the inner parts, in both modes: on phones they land on the sheet's parts, which carry the same data-slot="dialog-*" names (the backdrop and the handle keep drawer-overlay / drawer-handle).
classNamestringExtra classes for the card (dialog-content), merged last.
Compound API: holds the state and picks the mode (a Radix dialog, or the drawer's sheet below 768px). No DOM. Rendered inside another dialog's content it nests: the parent steps back while it is open. Props: { open, defaultOpen, onOpenChange, dismissible, responsive, modal, shortcut, container }.
The portal, the backdrop (dialog-overlay), the card (dialog-content with data-size, data-scroll-behavior, data-nested, data-child-open) and the ×. Needs a DialogTitle inside (visuallyHidden if the design has none); pass aria-describedby={undefined} when there is no description. Props: { size, radius, scrollBehavior, showClose, closeLabel, container, classNames } & Radix Dialog.Content props.
The icon tile, title and description; keeps clear of the ×. Its bottom hairline fades in once the body scrolls. Without a title (and no children of its own) it renders a hidden “Dialog” title. Props: { title?, description?, hideTitle?, icon?, tone?, color?, align?: "start" | "center" }.
18px semibold title (17px on the sheet), wired to aria-labelledby. Props: { visuallyHidden?: boolean } & Radix Dialog.Title props.
A muted 14px description, wired to aria-describedby. Takes Radix Dialog.Description props.
The 40px tone tile on its own, for custom headers. Props: { tone?, color? } & ComponentProps<"span">.
The scroll region between header and footer. Reports its scroll edges for the hairlines; on the sheet it scrolls first and only then lets the sheet drag. A long text with nothing focusable inside joins the Tab order (with a ring) so the arrow and Page keys scroll it. Takes div props.
Opens the dialog from any element. Takes Radix Dialog.Trigger props (asChild).
Closes the dialog from any element, also one that is not dismissible. Takes Radix Dialog.Close props (asChild).
The nearest dialog's state, e.g. to close it after a form saves, or to lay out differently on the phone sheet. Signature: () => { open: boolean, setOpen: (open: boolean) => void, sheet: boolean }.
sm to xl: 400, 520, 720 and 960px. The same content reflows to the width.
Inside pins the header and footer, with hairlines that follow the scroll; outside lets the whole card scroll with the page.
Pick the city from a second dialog: the address form steps back while it is open.
A success tile, a centered header and a share action, opened after a fake 1s request.
dismissible={false} and no ×: Esc and outside clicks only bump the card until you choose.
size="full" keeps 16px from every edge; a full-height sheet on phones. Arrow keys page through the photos.
shortcut="mod+e" toggles it from anywhere; the trigger shows the key and gets aria-keyshortcuts.
Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.
Run npx wingo-ui@latest add dialog. It also adds the Wingo UI items it builds on (Button, Drawer, Motion presets, Tones, useControllableState and 4 more) and the npm packages @radix-ui/react-dialog, @radix-ui/react-visually-hidden, lucide-react and motion. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/dialog.json.
Below 768px it opens as the drawer's bottom sheet: drag to dismiss, safe-area padding, a 92% height for size full. Off, it stays a centered card on phones too. The responsive prop controls this and is on by default.
Building UI with AI agents11 min read
Building UI with AI agents10 min read
Building UI with AI agents10 min read
Comparisons and alternatives10 min read

Pegas city bike
$420
Brooklyn · 214 views
Pegas City 1, medium frame, 3-speed hub. Ridden for two summers, serviced in May. Comes with a basket, a lock and a battery light.
Click Edit to open it. Press Esc, click outside or use × (“Close”) to close it. A long form scrolls between the pinned header and footer. Below 768px it opens as a bottom sheet.