ComponentsOverlays
Modal
A dialog over a dimmed page, in four sizes, with a scrolling body and a confirm variant.
Form
"use client";
import { Pencil } from "lucide-react";
import { Button } from "@/components/miyuki-ui/button";
import { TextField } from "@/components/miyuki-ui/input";
import {
Modal,
ModalBody,
ModalDescription,
ModalFooter,
ModalHeader,
ModalTitle,
ModalTrigger,
} from "@/components/miyuki-ui/modal";
export default function ModalDemo() {
return (
<ModalTrigger>
<Button variant="secondary" iconStart={<Pencil />}>
Rename route
</Button>
<Modal>
{({ close }) => (
<form
className="contents"
onSubmit={(event) => {
event.preventDefault();
close();
}}
>
<ModalHeader>
<ModalTitle>Rename this route</ModalTitle>
<ModalDescription>
Everyone on the Hakuba trip will see the new name the next time they open the map.
</ModalDescription>
</ModalHeader>
<ModalBody>
<TextField label="Route name" defaultValue="Hakuba traverse" autoFocus isRequired />
</ModalBody>
<ModalFooter>
<Button variant="secondary" slot="close">
Cancel
</Button>
<Button type="submit">Save</Button>
</ModalFooter>
</form>
)}
</Modal>
</ModalTrigger>
);
}Alert dialog
"use client";
import { Trash2 } from "lucide-react";
import { Button } from "@/components/miyuki-ui/button";
import { AlertDialog, ModalTrigger } from "@/components/miyuki-ui/modal";
export default function ModalAlertDemo() {
return (
<ModalTrigger>
<Button variant="secondary" iconStart={<Trash2 />}>
Delete route
</Button>
<AlertDialog
tone="destructive"
title="Delete Hakuba traverse?"
actionLabel="Delete route"
onAction={() => new Promise((resolve) => setTimeout(resolve, 900))}
>
The route and its 24 waypoints will be removed for everyone. This cannot be undone.
</AlertDialog>
</ModalTrigger>
);
}Scrolling content
"use client";
import { FileText } from "lucide-react";
import { Button } from "@/components/miyuki-ui/button";
import {
Modal,
ModalBody,
ModalDescription,
ModalFooter,
ModalHeader,
ModalTitle,
ModalTrigger,
} from "@/components/miyuki-ui/modal";
const terms = [
{
title: "Bookings",
text: "A place on a guided trip is held once the deposit of ¥12,000 is paid. The balance is due 21 days before the first day. We send a reminder a week before it falls due, by email and in the app.",
},
{
title: "Weather and snow",
text: "Our guides decide each morning whether a route goes ahead, using the avalanche bulletin, the overnight snowfall and their own pit tests. If a day is cancelled for conditions we offer another route of the same grade, a place on a later date, or a refund for that day.",
},
{
title: "Cancelling",
text: "Cancel more than 30 days ahead and we return everything except the deposit. Between 30 and 8 days we keep half of the trip price. Inside a week we cannot refund, but you may pass your place to a friend who meets the grade.",
},
{
title: "Equipment",
text: "Avalanche transceiver, shovel and probe are required on every winter route and can be borrowed from the shop in Hakuba at no charge. Skis, skins and boots are yours to bring or rent. Your guide checks every transceiver at the trailhead.",
},
{
title: "Fitness and grades",
text: "Each route carries a grade from one to four. Grade three and above assume you can climb 1,100 metres in a day with a pack and ski variable snow in control. If your guide judges that the group is moving too slowly to return safely, the day will turn early.",
},
{
title: "Insurance",
text: "You need travel insurance that covers off-piste skiing and mountain rescue in Japan. Please bring the policy number on the first morning; we cannot take you out without it.",
},
{
title: "Photographs",
text: "Guides sometimes take photographs on the hill. We only use them on our own pages, and never where you can be recognised unless you say yes on the day.",
},
];
export default function ModalScrollingDemo() {
return (
<ModalTrigger>
<Button variant="secondary" iconStart={<FileText />}>
Read trip terms
</Button>
<Modal size="md">
<ModalHeader>
<ModalTitle>Guided trip terms</ModalTitle>
<ModalDescription>Kitakaze Outfitters · Updated 3 November 2026</ModalDescription>
</ModalHeader>
<ModalBody className="flex flex-col gap-5 py-4">
{terms.map((term) => (
<section key={term.title} className="flex flex-col gap-1">
<h3 className="text-sm font-medium text-foreground">{term.title}</h3>
<p className="text-pretty text-muted-foreground">{term.text}</p>
</section>
))}
</ModalBody>
<ModalFooter>
<Button variant="secondary" slot="close">
Not now
</Button>
<Button slot="close">Accept terms</Button>
</ModalFooter>
</Modal>
</ModalTrigger>
);
}Sizes
This example uses generated pictures from this site: art.ts. Copy that file from the repository, or use your own images in its place.
"use client";
import { Button } from "@/components/miyuki-ui/button";
import {
Modal,
ModalBody,
ModalDescription,
ModalFooter,
ModalHeader,
type ModalSize,
ModalTitle,
ModalTrigger,
} from "@/components/miyuki-ui/modal";
import { landscape } from "@/registry/lib/art";
const sizes: { size: ModalSize; label: string }[] = [
{ size: "sm", label: "Small" },
{ size: "md", label: "Medium" },
{ size: "lg", label: "Large" },
{ size: "full", label: "Full" },
];
export default function ModalSizesDemo() {
return (
<div className="flex flex-wrap items-center justify-center gap-3">
{sizes.map(({ size, label }) => (
<ModalTrigger key={size}>
<Button variant="secondary">{label}</Button>
<Modal size={size}>
<ModalHeader>
<ModalTitle>Kiso valley, day two</ModalTitle>
<ModalDescription>Tsumago to Magome on the old Nakasendō, 7.8 km.</ModalDescription>
</ModalHeader>
<ModalBody className="flex flex-col">
<img
src={landscape("kiso", { width: 960, height: 540, mood: "snow" })}
alt="Snow on the ridges above the Kiso valley"
className={
size === "full"
? "min-h-0 w-full flex-1 rounded-lg object-cover"
: "aspect-video w-full rounded-lg object-cover"
}
/>
</ModalBody>
<ModalFooter>
<Button slot="close">Done</Button>
</ModalFooter>
</Modal>
</ModalTrigger>
))}
</div>
);
}Blurred backdrop
"use client";
import { CloudSnow } from "lucide-react";
import { Button } from "@/components/miyuki-ui/button";
import {
Modal,
ModalBody,
ModalDescription,
ModalFooter,
ModalHeader,
ModalTitle,
ModalTrigger,
} from "@/components/miyuki-ui/modal";
export default function ModalBlurDemo() {
return (
<ModalTrigger>
<Button variant="secondary" iconStart={<CloudSnow />}>
Snow report
</Button>
<Modal backdropBlur>
<ModalHeader>
<ModalTitle>Hakuba, this morning</ModalTitle>
<ModalDescription>Posted at 06:40 by the ski patrol at Happo-one.</ModalDescription>
</ModalHeader>
<ModalBody>
<p>
18 cm of new snow overnight on a 212 cm base. The upper lifts open at 08:30, and the Kokusai gondola stays
on wind hold until noon.
</p>
</ModalBody>
<ModalFooter>
<Button slot="close">Done</Button>
</ModalFooter>
</Modal>
</ModalTrigger>
);
}Install
npx shadcn@latest add @miyuki-ui/modalThis needs the @miyuki-ui registry in your components.json. See Using the shadcn CLI.
Props
ModalTrigger
Prop
Type
Modal
Prop
Type
ModalHeader
Prop
Type
ModalTitle
Prop
Type
ModalDescription
Prop
Type
ModalBody
Prop
Type
ModalFooter
AlertDialog
Prop
Type