Variants
Learn how to build and customize modal variants
Variants
Variants are the shells that render your modal content.
Key ideas
- Variants are just components: they receive your modal props and children, and they render whatever UI you want.
- You can fully customize the layout, transitions, and interaction model for each variant.
- Helper hooks such as
useModalanduseModalStoregive you access to the active modal's metadata and lifecycle helpers inside the variant body. - Any component library works for variants—compose it with Better Modal's hooks to control open/close state.
Hooks
useModal()returns the active modal instanceuseModalStore()exposes helpers likedismissandremoveto orchestrate closing and cleanup.
const modal = useModal();
const store = useModalStore();
store.dismiss(modal.id); // Close the modal
store.remove(modal.id); // Remove once your exit animation completesRegistering variants
Once you have a variant component, register it when creating your modal collection:
import { betterModal } from "better-modal";
import { ShadcnDialog } from "@/modals/variants/shadcn-dialog";
const m = betterModal({
variants: {
dialog: ShadcnDialog,
},
});Every modal you create can target any registered variant, letting you reuse variant shells across different pieces of modal content.
Examples
Shadcn
Dialog
Basic
"use client";
import { useModal, useModalStore } from "better-modal/react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
type Props = {
title: string;
description: string;
};
export function ShadcnDialog({
children,
title,
description,
}: { children: React.ReactNode } & Props) {
const modal = useModal();
const store = useModalStore();
return (
<Dialog
open={modal.open}
onOpenChange={(v) => !v && store.dismiss(modal.id)}
>
<DialogContent
onAnimationEnd={() => {
if (!modal.open) store.remove(modal.id);
}}
>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
{children}
</DialogContent>
</Dialog>
);
}
With Sizes
"use client";
import { useModal, useModalStore } from "better-modal/react";
import { cva, type VariantProps } from "class-variance-authority";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
const dialogVariants = cva("", {
variants: {
size: {
sm: "sm:max-w-sm",
md: "sm:max-w-md",
lg: "max-w-lg",
xl: "sm:max-w-xl",
"2xl": "sm:max-w-2xl",
"3xl": "sm:max-w-3xl",
"4xl": "sm:max-w-4xl",
"5xl": "sm:max-w-5xl",
"6xl": "sm:max-w-6xl",
"7xl": "sm:max-w-7xl",
"8xl": "sm:max-w-8xl",
"9xl": "sm:max-w-9xl",
full: "sm:max-w-full h-full",
},
},
});
type Props = {
title: string;
description: string;
} & VariantProps<typeof dialogVariants>;
export function ShadcnDialogSizes({
children,
title,
description,
size,
}: { children: React.ReactNode } & Props) {
const modal = useModal();
const store = useModalStore();
return (
<Dialog
open={modal.open}
onOpenChange={(v) => !v && store.dismiss(modal.id)}
>
<DialogContent
className={dialogVariants({ size })}
onAnimationEnd={() => {
if (!modal.open) store.remove(modal.id);
}}
>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
{children}
</DialogContent>
</Dialog>
);
}
Sheet
Basic
"use client";
import { useModal, useModalStore } from "better-modal/react";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
type Props = {
title: string;
description: string;
};
export function ShadcnSheet({
children,
title,
description,
}: { children: React.ReactNode } & Props) {
const modal = useModal();
const store = useModalStore();
return (
<Sheet
open={modal.open}
onOpenChange={(v) => !v && store.dismiss(modal.id)}
>
<SheetContent
onAnimationEnd={() => {
if (!modal.open) store.remove(modal.id);
}}
>
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
<SheetDescription>{description}</SheetDescription>
</SheetHeader>
{children}
</SheetContent>
</Sheet>
);
}
With Sizes
"use client";
import { useModal, useModalStore } from "better-modal/react";
import { cva, type VariantProps } from "class-variance-authority";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
const sheetVariants = cva("", {
variants: {
size: {
sm: "sm:max-w-sm",
md: "sm:max-w-md",
lg: "max-w-lg",
xl: "sm:max-w-xl",
"2xl": "sm:max-w-2xl",
"3xl": "sm:max-w-3xl",
"4xl": "sm:max-w-4xl",
"5xl": "sm:max-w-5xl",
"6xl": "sm:max-w-6xl",
"7xl": "sm:max-w-7xl",
full: "w-full",
},
},
});
type Props = {
title: string;
description: string;
side?: "top" | "right" | "bottom" | "left";
} & VariantProps<typeof sheetVariants>;
export function ShadcnSheetSizes({
children,
title,
description,
size,
side = "right",
}: { children: React.ReactNode } & Props) {
const modal = useModal();
const store = useModalStore();
return (
<Sheet
open={modal.open}
onOpenChange={(v) => !v && store.dismiss(modal.id)}
>
<SheetContent
side={side}
data-side={side}
className={sheetVariants({ size })}
onAnimationEnd={() => {
if (!modal.open) store.remove(modal.id);
}}
>
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
<SheetDescription>{description}</SheetDescription>
</SheetHeader>
{children}
</SheetContent>
</Sheet>
);
}
Alert Dialog
Basic
"use client";
import { useModal, useModalStore } from "better-modal/react";
import {
AlertDialog,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
type Props = {
title: string;
description: string;
};
export function ShadcnAlertDialog({
children,
title,
description,
}: { children: React.ReactNode } & Props) {
const modal = useModal();
const store = useModalStore();
return (
<AlertDialog
open={modal.open}
onOpenChange={(v) => !v && store.dismiss(modal.id)}
>
<AlertDialogContent
onAnimationEnd={() => {
if (!modal.open) store.remove(modal.id);
}}
>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
{children}
</AlertDialogContent>
</AlertDialog>
);
}
With Action
"use client";
import { useModal, useModalStore } from "better-modal/react";
import { useState, useTransition } from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
type Props = {
title: string;
description: string;
confirmText?: string;
cancelText?: string;
onConfirm?: () => void | Promise<void>;
onCancel?: () => void;
onError?: (e: unknown) => void | Promise<void>;
children: React.ReactNode;
};
export function ShadcnAlertDialogAction({
title,
description,
children,
onConfirm,
onCancel,
onError,
confirmText = "Confirm",
cancelText = "Cancel",
}: Props) {
const modal = useModal();
const store = useModalStore();
const [isPending, startTransition] = useTransition();
const handleConfirm = async (e: React.MouseEvent) => {
if (!onConfirm) return;
e.preventDefault();
startTransition(async () => {
try {
const result = onConfirm();
if (result instanceof Promise) {
await result;
}
store.dismiss(modal.id);
} catch (e) {
onError?.(e);
}
});
};
return (
<AlertDialog
open={modal.open}
onOpenChange={(v) => !v && store.dismiss(modal.id)}
>
<AlertDialogContent
onAnimationEnd={() => {
if (!modal.open) store.remove(modal.id);
}}
>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
{children}
<AlertDialogFooter>
<AlertDialogCancel
onClick={() => {
store.dismiss(modal.id);
onCancel?.();
}}
>
{cancelText}
</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirm} disabled={isPending}>
{confirmText}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}