Introduction

Toldo is an elevated dialog component built on top of @radix-ui that provides a few extra features to make it easier to work with dialogs in your application.

Installation

Install the component from your command line.

terminal
pnpm install toldo

Anatomy

Import all parts and piece them together.

dialog.tsx
import * as Dialog from "@radix-ui/react-dialog";
 
export default () => (
  <Dialog.Root>
    <Dialog.Trigger>
      <Dialog.SharedItem>
    </Dialog.Trigger>
    <Dialog.Portal>
      <Dialog.Overlay />
      <Dialog.Content>
        <Dialog.Title />
        <Dialog.Description />
        <Dialog.Close />
      </Dialog.Content>
      <Dialog.Stack>
        <Dialog.StackContent>
          <Dialog.StackTitle />
          <Dialog.StackDescription />
          <Dialog.StackAdd />
		  <Dialog.StackRemove />
        </Dialog.StackContent>
      </Dialog.Stack>
    </Dialog.Portal>
  </Dialog.Root>
);

Examples

Basic

This example demonstrates the simplest use of Toldo's dialog component. This setup uses straightforward styles and functional controls to provide a basic dialog interaction.

basic.tsx
import * as Dialog from "toldo";
 
export const Basic = () => {
  return (
    <Dialog.Root>
      <Dialog.Trigger className="h-[32px] rounded-lg border border-gray-3 bg-gradient-to-t bg-gray-1 from-gray-1 to-gray-2 px-3 transition-all ease-in-out hover:brightness-95">
        Open Dialog
      </Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay className=" fixed inset-0 bg-black-a10" />
        <Dialog.Content className="-translate-x-1/2 -translate-y-1/2 fixed top-1/2 left-1/2 max-h-[85vh] w-[90vw] max-w-[450px] flex-col overflow-hidden rounded-xl border border-gray-3 bg-gray-1 sm:w-[384px]">
          <Dialog.Title className="px-6 pt-5 font-semibold text-foreground text-large">Change Username</Dialog.Title>
          <Dialog.Description className="px-6 py-1 text-default text-muted">
            Make changes to your username here.
          </Dialog.Description>
          <fieldset className="mb-[15px] flex items-center gap-4 px-6 py-4">
            <input
              id="name"
              placeholder="@raphaelsalaja"
              className="inline-flex h-[32px] w-full flex-1 items-center justify-center rounded-lg border border-gray-4 bg-gray-2 px-2.5 text-[15px] text-default leading-none transition-all placeholder:text-muted focus:outline-none focus:ring-1 focus:ring-green-11 focus:ring-offset-2 focus:ring-offset-gray-1"
            />
          </fieldset>
          <div className="flex justify-between gap-4 border-gray-3 border-t bg-gray-2 px-6 py-5">
            <Dialog.Close className="!text-gray-11 h-[32px] max-w-fit rounded-lg bg-gray-a3 px-3 transition-all ease-in-out hover:brightness-150">
              Cancel
            </Dialog.Close>
            <Dialog.Close className="!text-green-11 h-[32px] max-w-fit rounded-lg bg-green-a3 px-3 transition-all ease-in-out hover:brightness-150">
              Save Changes
            </Dialog.Close>
          </div>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
};

Basic with Animation

In this example, we add animations to the basic dialog for a more engaging experience. Using Framer Motion, we apply smooth fade-in and scaling effects when the dialog opens and closes.

In order for the animation presence to work, you need to make sure to mount the portal components when the dialog is open. This is done by setting the forceMount prop to true on the Dialog.Portal component.

basic-with-animation.tsx
"use client";
 
import { AnimatePresence, type AnimationProps, motion } from "framer-motion";
import React from "react";
import * as Dialog from "toldo";
 
export const BasicWithAnimation = () => {
  const [open, setOpen] = React.useState(false);
 
  const variants: { [key: string]: AnimationProps } = {
    overlay: {
      initial: { opacity: 0 },
      animate: { opacity: 1 },
      exit: { opacity: 0 },
      transition: { ease: [0.19, 1, 0.22, 1], duration: 0.4 },
    },
    content: {
      initial: { scale: 0.9, opacity: 0 },
      animate: { scale: 1, opacity: 1 },
      exit: { scale: 0.9, opacity: 0 },
      transition: { ease: [0.19, 1, 0.22, 1], duration: 0.4 },
    },
  };
 
  return (
    <Dialog.Root open={open} onOpenChange={setOpen}>
      <Dialog.Trigger className="h-[32px] rounded-lg border border-gray-3 bg-gradient-to-t bg-gray-1 from-gray-1 to-gray-2 px-3 transition-all ease-in-out hover:brightness-95">
        Open Dialog
      </Dialog.Trigger>
      <Dialog.Portal forceMount>
        <AnimatePresence mode="popLayout">
          {open && (
            <Dialog.Overlay className="fixed top-0 left-0 h-full w-full">
              <motion.div className="fixed inset-0 bg-black-a10" {...variants.overlay} />
            </Dialog.Overlay>
          )}
        </AnimatePresence>
        <AnimatePresence mode="popLayout">
          {open && (
            <Dialog.Content className="-translate-x-1/2 -translate-y-1/2 fixed top-1/2 left-1/2 max-h-[85vh] w-[90vw] max-w-[450px]">
              <motion.div
                className="flex-col overflow-hidden rounded-xl border border-gray-3 bg-gray-1 sm:w-[384px]"
                {...variants.content}
              >
                <Dialog.Title className="px-6 pt-5 font-semibold text-foreground text-large">
                  Change Username
                </Dialog.Title>
                <Dialog.Description className="px-6 py-1 text-default text-muted">
                  Make changes to your username here.
                </Dialog.Description>
                <fieldset className="mb-[15px] flex items-center gap-4 px-6 py-4">
                  <input
                    id="name"
                    placeholder="@raphaelsalaja"
                    className="inline-flex h-[32px] w-full flex-1 items-center justify-center rounded-lg border border-gray-4 bg-gray-2 px-2.5 text-[15px] text-default leading-none transition-all placeholder:text-muted focus:outline-none focus:ring-1 focus:ring-green-11 focus:ring-offset-2 focus:ring-offset-gray-1"
                  />
                </fieldset>
                <div className="flex justify-between gap-4 border-gray-3 border-t bg-gray-2 px-6 py-5">
                  <Dialog.Close className="!text-gray-11 h-[32px] max-w-fit rounded-lg bg-gray-a3 px-3 transition-all ease-in-out hover:brightness-150">
                    Cancel
                  </Dialog.Close>
                  <Dialog.Close className="!text-green-11 h-[32px] max-w-fit rounded-lg bg-green-a3 px-3 transition-all ease-in-out hover:brightness-150">
                    Save Changes
                  </Dialog.Close>
                </div>
              </motion.div>
            </Dialog.Content>
          )}
        </AnimatePresence>
      </Dialog.Portal>
    </Dialog.Root>
  );
};

Stacked

Stacked dialogs require a different setup. Them main diffrence is that we create our dialogs in an array and pass them to the Dialog.Root component. Use the Dialog.Stack component to manage the dialogs in the stack.

We use the Dialog.StackAdd and Dialog.StackRemove components to add and remove dialogs from the stack. Be mindful to include the dialogId prop to identify the dialog you want to add or remove within the trigger.

Note that modal dialogs interrupt users and demand an action. They are appropriate when user’s attention needs to be directed toward important information. It is not reccomended to use stacked dialogs for simple tasks. Only use them when the user needs to complete a series of tasks or decisions.

stacked.tsx
"use client";
 
import { AnimatePresence, type AnimationProps, motion } from "framer-motion";
import { useState } from "react";
import * as Dialog from "toldo";
 
export const Stacked = () => {
  const [open, setOpen] = useState(false);
 
  const dialogs: Dialog.Props[] = [
    {
      id: "username",
      dialog: (
        <Dialog.StackContent className="flex max-h-[85vh] w-[90vw] flex-col overflow-hidden rounded-xl border border-gray-3 bg-gray-1 sm:w-[384px]">
          <Dialog.StackTitle className="px-6 pt-5 font-semibold text-foreground text-large">
            Change Username
          </Dialog.StackTitle>
          <Dialog.StackDescription className="px-6 py-1 text-default text-muted">
            Make changes to your username here.
          </Dialog.StackDescription>
          <fieldset className="mb-[15px] flex items-center gap-4 px-6 py-5">
            <input
              id="name"
              placeholder="@raphaelsalaja"
              className="inline-flex h-[32px] w-full flex-1 items-center justify-center rounded-lg border border-gray-4 bg-gray-2 px-2.5 text-[15px] text-default leading-none transition-all placeholder:text-muted focus:outline-none focus:ring-1 focus:ring-green-11 focus:ring-offset-2 focus:ring-offset-gray-1"
            />
          </fieldset>
          <div className="flex justify-between gap-4 border-gray-3 border-t bg-gray-2 px-6 py-4">
            <Dialog.Close className="!text-gray-11 h-[32px] max-w-fit rounded-lg bg-gray-a3 px-3 transition-all ease-in-out hover:brightness-150">
              Cancel
            </Dialog.Close>
            <Dialog.StackAdd
              dialogId="email"
              className="!text-blue-11 h-[32px] max-w-fit rounded-lg bg-blue-a3 px-3 transition-all ease-in-out hover:brightness-150"
            >
              Continue
            </Dialog.StackAdd>
          </div>
        </Dialog.StackContent>
      ),
    },
    {
      id: "email",
      dialog: (
        <Dialog.StackContent className="flex max-h-[85vh] w-[90vw] flex-col overflow-hidden rounded-xl border border-gray-3 bg-gray-1 sm:w-[384px]">
          <Dialog.StackTitle className="px-6 pt-5 font-semibold text-foreground text-large">
            Change Email
          </Dialog.StackTitle>
          <Dialog.StackDescription className="px-6 py-1 text-default text-muted">
            Make changes to your email here.
          </Dialog.StackDescription>
          <fieldset className="mb-[15px] flex items-center gap-4 px-6 py-5">
            <input
              id="email"
              placeholder="raphaelsalaja@gmail.com"
              className="inline-flex h-[32px] w-full flex-1 items-center justify-center rounded-lg border border-gray-4 bg-gray-2 px-2.5 text-[15px] text-default leading-none transition-all placeholder:text-muted focus:outline-none focus:ring-1 focus:ring-green-11 focus:ring-offset-2 focus:ring-offset-gray-1"
            />
          </fieldset>
          <div className="flex justify-between gap-4 border-gray-3 border-t bg-gray-2 px-6 py-4">
            <Dialog.StackRemove
              dialogId="email"
              className="!text-gray-11 h-[32px] max-w-fit rounded-lg bg-gray-a3 px-3 transition-all ease-in-out hover:brightness-150"
            >
              Return
            </Dialog.StackRemove>
            <Dialog.StackAdd
              dialogId="date-of-birth"
              className="!text-blue-11 h-[32px] max-w-fit rounded-lg bg-blue-a3 px-3 transition-all ease-in-out hover:brightness-150"
            >
              Continue
            </Dialog.StackAdd>
          </div>
        </Dialog.StackContent>
      ),
    },
    {
      id: "date-of-birth",
      dialog: (
        <Dialog.StackContent className="flex max-h-[85vh] w-[90vw] flex-col overflow-hidden rounded-xl border border-gray-3 bg-gray-1 sm:w-[384px]">
          <Dialog.StackTitle className="px-6 pt-5 font-semibold text-foreground text-large">
            Change Date of Birth
          </Dialog.StackTitle>
          <Dialog.StackDescription className="px-6 py-1 text-default text-muted">
            Make changes to your birth date here.
          </Dialog.StackDescription>
          <fieldset className="mb-[15px] flex items-center gap-4 px-6 py-5">
            <input
              id="date-of-birth"
              placeholder="January 1, 2000"
              className="inline-flex h-[32px] w-full flex-1 items-center justify-center rounded-lg border border-gray-4 bg-gray-2 px-2.5 text-[15px] text-default leading-none transition-all placeholder:text-muted focus:outline-none focus:ring-1 focus:ring-green-11 focus:ring-offset-2 focus:ring-offset-gray-1"
            />
          </fieldset>
          <div className="flex justify-between gap-4 border-gray-3 border-t bg-gray-2 px-6 py-4">
            <Dialog.StackRemove
              dialogId="date-of-birth"
              className="!text-gray-11 h-[32px] max-w-fit rounded-lg bg-gray-a3 px-3 transition-all ease-in-out hover:brightness-150"
            >
              Return
            </Dialog.StackRemove>
            <Dialog.Close className="!text-green-11 h-[32px] max-w-fit rounded-lg bg-green-a3 px-3 transition-all ease-in-out hover:brightness-150">
              Save Changes
            </Dialog.Close>
          </div>
        </Dialog.StackContent>
      ),
    },
  ];
 
  const variants: { [key: string]: AnimationProps } = {
    overlay: {
      initial: { opacity: 0 },
      animate: { opacity: 1 },
      exit: { opacity: 0 },
      transition: { ease: [0.19, 1, 0.22, 1], duration: 0.4 },
    },
    content: {
      initial: { scale: 0.9, opacity: 0 },
      animate: { scale: 1, opacity: 1 },
      exit: { scale: 0.9, opacity: 0 },
      transition: { ease: [0.19, 1, 0.22, 1], duration: 0.4 },
    },
  };
 
  return (
    <Dialog.Root open={open} onOpenChange={setOpen} dialogs={dialogs}>
      <Dialog.Trigger
        dialogId="username"
        className="h-[32px] rounded-lg border border-gray-3 bg-gradient-to-t bg-gray-1 from-gray-1 to-gray-2 px-3 transition-all ease-in-out hover:brightness-95"
      >
        Open Dialog
      </Dialog.Trigger>
      <Dialog.Portal forceMount>
        <AnimatePresence>
          {open && (
            <Dialog.Overlay className="fixed top-0 left-0 h-full w-full">
              <motion.div className="fixed inset-0 bg-black-a10" {...variants.overlay} />
            </Dialog.Overlay>
          )}
        </AnimatePresence>
        <AnimatePresence>{open && <Dialog.Stack {...variants.content} />}</AnimatePresence>
      </Dialog.Portal>
    </Dialog.Root>
  );
};

Shared Layout

The Dialog.SharedItem component is a handy component to share animations between dialogs. It wraps Framer's motion.div layout into a more readable component. For more information on shared layout transitions, check out the docs on Framer Motion.

shared.tsx
"use client";
import { AnimatePresence, type AnimationProps, motion } from "framer-motion";
import React from "react";
import * as Dialog from "toldo";
 
export const Shared = () => {
  const [open, setOpen] = React.useState(false);
 
  const variants: { [key: string]: AnimationProps } = {
    overlay: {
      initial: { opacity: 0 },
      animate: { opacity: 1 },
      exit: { opacity: 0 },
      transition: { ease: [0.19, 1, 0.22, 1], duration: 0.4 },
    },
    content: {
      initial: { scale: 0.9, opacity: 0 },
      animate: { scale: 1, opacity: 1 },
      exit: { scale: 0.9, opacity: 0 },
      transition: { ease: [0.19, 1, 0.22, 1], duration: 0.4 },
    },
    button: {
      transition: { layout: { ease: [0.19, 1, 0.22, 1], duration: 0.6 } },
    },
  };
 
  return (
    <Dialog.Root open={open} onOpenChange={setOpen}>
      <Dialog.Trigger>
        <Dialog.SharedItem
          layout
          layoutId="action"
          className="pointer-events-auto flex h-[32px] items-center rounded-lg border border-gray-3 bg-gradient-to-t bg-gray-1 from-gray-1 to-gray-2 px-3"
          {...variants.button}
        >
          Open Dialog
        </Dialog.SharedItem>
      </Dialog.Trigger>
 
      <AnimatePresence>
        {open && (
          <Dialog.Portal forceMount>
            <Dialog.Overlay className="fixed top-0 left-0 h-full w-full">
              <motion.div className="fixed inset-0 bg-black-a10" {...variants.overlay} />
            </Dialog.Overlay>
            <Dialog.Content className="-translate-x-1/2 -translate-y-1/2 fixed top-1/2 left-1/2 max-h-[85vh] w-[90vw] max-w-[450px]">
              <motion.div
                className="w-full flex-col overflow-hidden rounded-xl border border-gray-3 bg-gray-1 sm:w-[384px]"
                {...variants.content}
              >
                <Dialog.Title className="px-6 pt-5 font-semibold text-foreground text-large">
                  Change Username
                </Dialog.Title>
                <Dialog.Description className="px-6 py-1 text-default text-muted">
                  Make changes to your username here.
                </Dialog.Description>
                <fieldset className="mb-[15px] flex items-center gap-4 px-6 py-4">
                  <input
                    id="name"
                    placeholder="@raphaelsalaja"
                    className="inline-flex h-[32px] w-full flex-1 items-center justify-center rounded-lg border border-gray-4 bg-gray-2 px-2.5 text-[15px] text-default leading-none transition-all placeholder:text-muted focus:outline-none focus:ring-1 focus:ring-green-11 focus:ring-offset-2 focus:ring-offset-gray-1"
                  />
                </fieldset>
                <div className="flex justify-between gap-4 border-gray-3 border-t bg-gray-2 px-6 py-5">
                  <Dialog.Close className="!text-gray-11 h-[32px] max-w-fit rounded-lg bg-gray-a3 px-3 transition-all ease-in-out hover:brightness-150">
                    Cancel
                  </Dialog.Close>
                  <motion.div layout layoutId="action" {...variants.button}>
                    <Dialog.Close className="flex h-[32px] items-center rounded-lg border border-gray-3 bg-gradient-to-t bg-gray-1 from-gray-1 to-gray-2 px-3">
                      Save Changes
                    </Dialog.Close>
                  </motion.div>
                </div>
              </motion.div>
            </Dialog.Content>
          </Dialog.Portal>
        )}
      </AnimatePresence>
    </Dialog.Root>
  );
};

Examples Tailwind Config

The examples above use @radix-ui/colors for color variables. The following file contains the color variables used in the examples above.

tailwind.config.ts
import type { Config } from "tailwindcss";
 
import plugin from "tailwindcss/plugin";
 
const config: Config = {
  important: true,
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./markdown/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      colors: {
        gray: {
          1: "var(--gray-1)",
          2: "var(--gray-2)",
          3: "var(--gray-3)",
          4: "var(--gray-4)",
          5: "var(--gray-5)",
          6: "var(--gray-6)",
          7: "var(--gray-7)",
          8: "var(--gray-8)",
          9: "var(--gray-9)",
          10: "var(--gray-10)",
          11: "var(--gray-11)",
          12: "var(--gray-12)",
          a1: "var(--gray-a1)",
          a2: "var(--gray-a2)",
          a3: "var(--gray-a3)",
          a4: "var(--gray-a4)",
          a5: "var(--gray-a5)",
          a6: "var(--gray-a6)",
          a7: "var(--gray-a7)",
          a8: "var(--gray-a8)",
          a9: "var(--gray-a9)",
          a10: "var(--gray-a10)",
          a11: "var(--gray-a11)",
          a12: "var(--gray-a12)",
        },
        black: {
          a1: "var(--black-a1)",
          a2: "var(--black-a2)",
          a3: "var(--black-a3)",
          a4: "var(--black-a4)",
          a5: "var(--black-a5)",
          a6: "var(--black-a6)",
          a7: "var(--black-a7)",
          a8: "var(--black-a8)",
          a9: "var(--black-a9)",
          a10: "var(--black-a10)",
          a11: "var(--black-a11)",
          a12: "var(--black-a12)",
        },
        white: {
          a1: "var(--white-a1)",
          a2: "var(--white-a2)",
          a3: "var(--white-a3)",
          a4: "var(--white-a4)",
          a5: "var(--white-a5)",
          a6: "var(--white-a6)",
          a7: "var(--white-a7)",
          a8: "var(--white-a8)",
          a9: "var(--white-a9)",
          a10: "var(--white-a10)",
          a11: "var(--white-a11)",
          a12: "var(--white-a12)",
        },
        pink: {
          1: "var(--pink-1)",
          2: "var(--pink-2)",
          3: "var(--pink-3)",
          4: "var(--pink-4)",
          5: "var(--pink-5)",
          6: "var(--pink-6)",
          7: "var(--pink-7)",
          8: "var(--pink-8)",
          9: "var(--pink-9)",
          10: "var(--pink-10)",
          11: "var(--pink-11)",
          12: "var(--pink-12)",
          a1: "var(--pink-a1)",
          a2: "var(--pink-a2)",
          a3: "var(--pink-a3)",
          a4: "var(--pink-a4)",
          a5: "var(--pink-a5)",
          a6: "var(--pink-a6)",
          a7: "var(--pink-a7)",
          a8: "var(--pink-a8)",
          a9: "var(--pink-a9)",
          a10: "var(--pink-a10)",
          a11: "var(--pink-a11)",
          a12: "var(--pink-a12)",
        },
        yellow: {
          1: "var(--yellow-1)",
          2: "var(--yellow-2)",
          3: "var(--yellow-3)",
          4: "var(--yellow-4)",
          5: "var(--yellow-5)",
          6: "var(--yellow-6)",
          7: "var(--yellow-7)",
          8: "var(--yellow-8)",
          9: "var(--yellow-9)",
          10: "var(--yellow-10)",
          11: "var(--yellow-11)",
          12: "var(--yellow-12)",
          a1: "var(--yellow-a1)",
          a2: "var(--yellow-a2)",
          a3: "var(--yellow-a3)",
          a4: "var(--yellow-a4)",
          a5: "var(--yellow-a5)",
          a6: "var(--yellow-a6)",
          a7: "var(--yellow-a7)",
          a8: "var(--yellow-a8)",
          a9: "var(--yellow-a9)",
          a10: "var(--yellow-a10)",
          a11: "var(--yellow-a11)",
          a12: "var(--yellow-a12)",
        },
        red: {
          1: "var(--red-1)",
          2: "var(--red-2)",
          3: "var(--red-3)",
          4: "var(--red-4)",
          5: "var(--red-5)",
          6: "var(--red-6)",
          7: "var(--red-7)",
          8: "var(--red-8)",
          9: "var(--red-9)",
          10: "var(--red-10)",
          11: "var(--red-11)",
          12: "var(--red-12)",
          a1: "var(--red-a1)",
          a2: "var(--red-a2)",
          a3: "var(--red-a3)",
          a4: "var(--red-a4)",
          a5: "var(--red-a5)",
          a6: "var(--red-a6)",
          a7: "var(--red-a7)",
          a8: "var(--red-a8)",
          a9: "var(--red-a9)",
          a10: "var(--red-a10)",
          a11: "var(--red-a11)",
          a12: "var(--red-a12)",
        },
        green: {
          1: "var(--green-1)",
          2: "var(--green-2)",
          3: "var(--green-3)",
          4: "var(--green-4)",
          5: "var(--green-5)",
          6: "var(--green-6)",
          7: "var(--green-7)",
          8: "var(--green-8)",
          9: "var(--green-9)",
          10: "var(--green-10)",
          11: "var(--green-11)",
          12: "var(--green-12)",
          a1: "var(--green-a1)",
          a2: "var(--green-a2)",
          a3: "var(--green-a3)",
          a4: "var(--green-a4)",
          a5: "var(--green-a5)",
          a6: "var(--green-a6)",
          a7: "var(--green-a7)",
          a8: "var(--green-a8)",
          a9: "var(--green-a9)",
          a10: "var(--green-a10)",
          a11: "var(--green-a11)",
          a12: "var(--green-a12)",
        },
        blue: {
          1: "var(--blue-1)",
          2: "var(--blue-2)",
          3: "var(--blue-3)",
          4: "var(--blue-4)",
          5: "var(--blue-5)",
          6: "var(--blue-6)",
          7: "var(--blue-7)",
          8: "var(--blue-8)",
          9: "var(--blue-9)",
          10: "var(--blue-10)",
          11: "var(--blue-11)",
          12: "var(--blue-12)",
          a1: "var(--blue-a1)",
          a2: "var(--blue-a2)",
          a3: "var(--blue-a3)",
          a4: "var(--blue-a4)",
          a5: "var(--blue-a5)",
          a6: "var(--blue-a6)",
          a7: "var(--blue-a7)",
          a8: "var(--blue-a8)",
          a9: "var(--blue-a9)",
          a10: "var(--blue-a10)",
          a11: "var(--blue-a11)",
          a12: "var(--blue-a12)",
        },
        background: "var(--bg)",
        foreground: "var(--fg)",
        muted: "var(--muted)",
        hover: "var(--hover)",
        border: "var(--border)",
        scrollbar: {
          thumb: "var(--scrollbar-thumb)",
          track: "var(--scrollbar-track)",
        },
        selection: {
          background: "var(--selection-background)",
          foreground: "var(--selection-foreground)",
        },
        highlight: {
          background: "var(--highlight-background)",
          foreground: "var(--highlight-foreground)",
        },
        kbd: {
          background: "var(--kbd-background)",
          foreground: "var(--kbd-foreground)",
          border: "var(--kbd-border)",
        },
      },
      fontFamily: {
        inter: ["var(--font-inter)"],
      },
      borderRadius: {
        small: "var(--radius-small)",
        base: "var(--radius-base)",
        large: "var(--radius-large)",
      },
    },
  },
  plugins: [
    plugin(({ addUtilities }) => {
      addUtilities({
        ".text-small": {
          fontSize: "12px",
          letterSpacing: "0.01px",
        },
        ".text-default": {
          fontSize: "14px",
          lineHeight: "21px",
          letterSpacing: "-0.09px",
        },
        ".text-large": {
          fontSize: "16px",
          lineHeight: "24px",
          letterSpacing: "-0.18px",
        },
        ".text-huge": {
          fontSize: "24px",
          lineHeight: "36px",
          letterSpacing: "-0.47px",
        },
      });
    }),
  ],
  darkMode: "class",
};
 
export default config;

API Reference

Root

Contains all the parts of a dialog.

PropTypeDefault
defaultOpen
boolean
-
open
boolean
-
onOpenChange
function
-
modal
boolean
true
dialogs
array
-

Trigger

The button that opens the dialog.

PropTypeDefault
asChild
boolean
false

Portal

When used, portals your overlay and content parts into the body element.

PropTypeDefault
forceMount
boolean
-
container
HTMLElement
document.body

Overlay

A layer that covers the inert portion of the view when the dialog is open.

PropTypeDefault
asChild
boolean
false
forceMount
boolean
-

Content

Contains content to be rendered in the open dialog.

PropTypeDefault
asChild
boolean
false
forceMount
boolean
-
onOpenAutoFocus
function
-
onCloseAutoFocus
function
-
onEscapeKeyDown
function
-
onPointerDownOutside
function
-
onInteractOutside
function
-

Close

The button that closes the dialog.

PropTypeDefault
asChild
boolean
false

Title

An accessible title to be announced when the dialog is opened.

PropTypeDefault
asChild
boolean
false

Description

An optional accessible description to be announced when the dialog is opened.

PropTypeDefault
asChild
boolean
false

Stack

A wrapper that holds the dialogs in a stack.

PropTypeDefault
offsetY
number
24
offsetScale
number
0.05
offsetOpacity
number
0.33
initial
object
-
animate
string
-
exit
string
-
transition
string
-

Stack Content

Contains content to be rendered in the open dialog within a stack.

No Additional Props

Stack Title

An accessible title to be announced when the dialog is opened within a stack.

No Additional Props

Stack Description

An optional accessible description to be announced when the dialog is opened within a stack.

No Additional Props

Stack Remove

The button that removes the dialog from the stack.

PropTypeDefault
dialogId
string
-

Stack Add

The button that adds a dialog to the stack.

PropTypeDefault
dialogId
string
-

Shared Item

A handy component to share animations between dialogs.

PropTypeDefault
layout
enum
-
layoutId
string
-

Considerations

Dialogs should be used sparingly and only when necessary. They can be disruptive to the user experience.

When using dialogs, ensure that they are accessible to all users. This includes making sure that the dialog can be navigated using the keyboard, that focus is managed correctly, and that the dialog is announced to screen readers.

When it comes to dialogs on mobile devices, consider using a full-screen modal instead of a dialog. This will provide a better user experience on smaller screens. Check out drawer components like Vaul by Emil for more information.