PixelDosa
Overlay Motion Primitivecore

Overview

Scrim and content primitives for dialogs, sheets and popovers.

Install

This example composes Overlay Motion Primitive — install the primitive, then copy the example below.

bash
npx shadcn@latest add @pixeldosa/overlay

Code

tsx
"use client";

import * as React from "react";

import { Button } from "@/registry/button/button";
import { AnimatePresence, OverlayContent, OverlayScrim, type OverlayPlacement } from "@/registry/overlay/overlay";

/**
 * Demonstrates the shared enter/exit signature across the placements Dialog, Sheet and
 * Drawer each use. Wired here with plain useState — real components layer Radix's
 * Dialog/Sheet/Popover primitives underneath for focus trapping and dismiss behaviour;
 * this demo shows the motion layer in isolation.
 */
export default function OverlayDemo() {
  const [open, setOpen] = React.useState<OverlayPlacement | null>(null);

  return (
    <div className="flex flex-col items-center gap-6">
      <div className="flex flex-wrap items-center justify-center gap-3">
        <Button variant="outline" onClick={() => setOpen("center")}>
          Center (Dialog)
        </Button>
        <Button variant="outline" onClick={() => setOpen("right")}>
          Right (Sheet)
        </Button>
        <Button variant="outline" onClick={() => setOpen("bottom")}>
          Bottom (Drawer)
        </Button>
      </div>

      <AnimatePresence>
        {open ? (
          <React.Fragment key={open}>
            <OverlayScrim onClick={() => setOpen(null)} />
            <OverlayContent
              placement={open}
              className={
                open === "center"
                  ? "fixed left-1/2 top-1/2 w-80 -translate-x-1/2 -translate-y-1/2 p-6"
                  : open === "right"
                    ? "fixed right-0 top-0 h-full w-72 rounded-none border-l p-6"
                    : "fixed inset-x-0 bottom-0 rounded-b-none p-6"
              }
            >
              <p className="text-sm font-medium">Placement: {open}</p>
              <p className="mt-1 text-sm text-muted-foreground">
                Same duration and easing tokens, different translate axis.
              </p>
              <Button size="sm" className="mt-4" onClick={() => setOpen(null)}>
                Close
              </Button>
            </OverlayContent>
          </React.Fragment>
        ) : null}
      </AnimatePresence>
    </div>
  );
}