# Scale-back drawer

> A frosted side menu slides in while the page behind it shrinks and dims, like on iOS. Swipe it away to close.

- Category: [Overlays](https://library.aimran.es/components/overlays)
- Built with: React + Motion
- Live editor: https://library.aimran.es/blocks/scale-back-drawer
- Library: AIMRAN Library (https://library.aimran.es), free, no sign-up, by Aimran Oukham

## Where to use it

This iOS-style navigation drawer suits apps where the menu is how people get around: a notes app, the dashboard of a booking tool or the account area of a small shop. Because the page shrinks back instead of disappearing, it's clear you're still in the same place, just peeking at the menu.

It opens from the left or the right, and a scale around 90% feels natural; go much lower and the page looks far away. The copied code takes the full screen height, so it's meant to wrap the whole app rather than a single section.

## Code

### React (Motion)

```tsx
import { useEffect, useRef, useState, type RefObject } from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { Archive, House, Menu, Star, Users, X } from "lucide-react";

const SECTIONS = [
  { id: "inicio", label: "Home", icon: House },
  { id: "destacadas", label: "Featured", icon: Star },
  { id: "compartidas", label: "Shared", icon: Users },
  { id: "archivo", label: "File", icon: Archive },
];

const NOTE_ART = "radial-gradient(120% 90% at 100% 0%, #ffc49b 0%, transparent 50%), linear-gradient(135deg, #f0527a 0%, #a855f7 60%, #6d5dfc 100%)";

const FOCUSABLE =
  'a[href],button:not([disabled]),input:not([disabled]),select,textarea,[tabindex]:not([tabindex="-1"])';

// Modal accesible: enfoca el primer control (o el marcado con data-autofocus), atrapa Tab, cierra con Esc y devuelve el foco
function useDialog(ref: RefObject<HTMLElement | null>, open: boolean, onClose: () => void) {
  const close = useRef(onClose);
  useEffect(() => {
    close.current = onClose;
  });
  useEffect(() => {
    if (!open) return;
    const previous = document.activeElement as HTMLElement | null;
    const node = ref.current;
    const first =
      node?.querySelector<HTMLElement>("[data-autofocus]") ?? node?.querySelector<HTMLElement>(FOCUSABLE);
    first?.focus({ preventScroll: true });
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        e.preventDefault();
        close.current();
        return;
      }
      if (e.key !== "Tab" || !node) return;
      const items = Array.from(node.querySelectorAll<HTMLElement>(FOCUSABLE));
      if (!items.length) return;
      const firstItem = items[0];
      const lastItem = items[items.length - 1];
      if (!firstItem || !lastItem) return;
      if (!node.contains(document.activeElement)) {
        e.preventDefault();
        firstItem.focus();
      } else if (e.shiftKey && document.activeElement === firstItem) {
        e.preventDefault();
        lastItem.focus();
      } else if (!e.shiftKey && document.activeElement === lastItem) {
        e.preventDefault();
        firstItem.focus();
      }
    };
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("keydown", onKey);
      previous?.focus({ preventScroll: true });
    };
  }, [open, ref]);
}

interface ScaleDrawerProps {
  side?: "left" | "right";
  /** Escala de la página al abrir, en % */
  scale?: number;
  radius?: number;
}

export default function ScaleDrawer({
  side = "left",
  scale = 90,
  radius = 28,
}: ScaleDrawerProps) {
  const [open, setOpen] = useState(false);
  const [section, setSection] = useState("inicio");
  const panel = useRef<HTMLDivElement>(null);
  const reduce = useReducedMotion();
  const transition = reduce ? { duration: 0 } : { type: "spring" as const, stiffness: 300, damping: 32, mass: 0.9 };
  const dir = side === "left" ? 1 : -1;
  const current = SECTIONS.find((s) => s.id === section) ?? SECTIONS[0]!;
  useDialog(panel, open, () => setOpen(false));

  return (
    // h-dvh: la raíz ocupa la ventana y hace de fondo negro; las capas son absolute dentro de ella
    <div className="relative h-dvh w-full overflow-hidden bg-black">
      <motion.div
        animate={
          open
            ? { scale: scale / 100, x: 40 * dir, borderRadius: radius, filter: "brightness(0.7)" }
            : { scale: 1, x: 0, borderRadius: 0, filter: "brightness(1)" }
        }
        transition={transition}
        style={{ transformOrigin: side === "left" ? "100% 50%" : "0% 50%" }}
        className="absolute inset-0 overflow-hidden bg-[#f5f5f7] text-[#1d1d1f] dark:bg-[#161618] dark:text-[#f5f5f7]"
      >
        <div inert={open} aria-hidden={open} className="mx-auto flex h-full max-w-md flex-col px-5 pt-4">
          <div className={"flex items-center justify-between " + (side === "right" ? "flex-row-reverse" : "")}>
            <button
              type="button"
              aria-label="Open menu"
              aria-expanded={open}
              aria-haspopup="dialog"
              onClick={() => setOpen(true)}
              className="grid h-10 w-10 place-items-center rounded-full bg-white shadow-[0_1px_2px_rgba(0,0,0,0.06),0_6px_16px_rgba(0,0,0,0.06)] transition active:scale-95 dark:bg-white/10 dark:shadow-none"
            >
              <Menu className="h-[18px] w-[18px]" strokeWidth={1.5} />
            </button>
            <span className="grid h-9 w-9 place-items-center rounded-full bg-gradient-to-br from-[#fcd34d] to-[#f97316] text-[12px] font-semibold text-white">
              ER
            </span>
          </div>
          <p className="mt-7 text-[13px] text-[#86868b]">Tuesday, September 26</p>
          <AnimatePresence mode="wait" initial={false}>
            <motion.h2
              key={current.id}
              initial={{ opacity: 0, y: 8 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0, y: -8 }}
              transition={{ duration: reduce ? 0 : 0.18 }}
              className="text-[32px] font-semibold leading-tight tracking-[-0.03em]"
            >
              {current.label}
            </motion.h2>
          </AnimatePresence>
          <div
            className="relative mt-5 overflow-hidden rounded-[22px] p-5 text-white shadow-[0_10px_30px_-10px_rgba(168,85,247,0.6)]"
            style={{ background: NOTE_ART }}
          >
            <p className="text-[12px] font-medium text-white/75">Pinned note</p>
            <p className="mt-7 text-[20px] font-semibold leading-tight tracking-[-0.02em]">Lisbon in four days</p>
            <p className="mt-1 text-[13px] text-white/75">Edited 2 min ago</p>
          </div>
          <div className="mt-3 grid grid-cols-2 gap-3">
            <div className="rounded-[20px] bg-white p-4 shadow-[0_1px_2px_rgba(0,0,0,0.04)] dark:bg-white/[0.06] dark:shadow-none">
              <p className="text-[22px] font-semibold leading-none tracking-[-0.03em]">12</p>
              <p className="mt-1.5 text-[13px] text-[#86868b]">Ideas</p>
            </div>
            <div className="rounded-[20px] bg-white p-4 shadow-[0_1px_2px_rgba(0,0,0,0.04)] dark:bg-white/[0.06] dark:shadow-none">
              <p className="text-[22px] font-semibold leading-none tracking-[-0.03em]">8</p>
              <p className="mt-1.5 text-[13px] text-[#86868b]">Recipes</p>
            </div>
          </div>
        </div>
      </motion.div>

      <AnimatePresence>
        {open && (
          <>
            <motion.div key="catcher" aria-hidden className="absolute inset-0" onClick={() => setOpen(false)} />
            <motion.div
              key="drawer"
              ref={panel}
              role="dialog"
              aria-modal="true"
              aria-label="Menu"
              initial={{ x: side === "left" ? "-110%" : "110%" }}
              animate={{ x: 0 }}
              exit={{ x: side === "left" ? "-110%" : "110%" }}
              transition={transition}
              drag="x"
              dragConstraints={{ left: 0, right: 0 }}
              dragElastic={side === "left" ? { left: 0.5, right: 0.05 } : { left: 0.05, right: 0.5 }}
              onDragEnd={(_, info) => {
                if (info.offset.x * dir < -70 || info.velocity.x * dir < -500) setOpen(false);
              }}
              className={"absolute inset-y-3 flex w-[min(78%,300px)] touch-pan-y flex-col rounded-[28px] border border-white/60 bg-white/80 p-3 text-[#1d1d1f] shadow-[0_8px_20px_rgba(0,0,0,0.15),0_30px_70px_rgba(0,0,0,0.35)] backdrop-blur-2xl backdrop-saturate-150 dark:border-white/10 dark:bg-[#232326]/80 dark:text-[#f5f5f7] " + (side === "left" ? "left-3" : "right-3")}
            >
              <div className="flex items-center justify-between px-2 pb-3 pt-1">
                <span className="flex items-center gap-2 text-[15px] font-semibold tracking-[-0.01em]">
                  <span className="h-6 w-6 rounded-[8px]" style={{ background: NOTE_ART }} />
                  Notes
                </span>
                <button
                  type="button"
                  aria-label="Close"
                  onClick={() => setOpen(false)}
                  className="grid h-8 w-8 place-items-center rounded-full bg-black/5 transition hover:bg-black/10 dark:bg-white/10 dark:hover:bg-white/20"
                >
                  <X className="h-4 w-4" strokeWidth={1.5} />
                </button>
              </div>
              <ul className="space-y-0.5">
                {SECTIONS.map(({ id, label, icon: Icon }) => (
                  <li key={id}>
                    <button
                      type="button"
                      aria-current={id === section ? "page" : undefined}
                      onClick={() => {
                        setSection(id);
                        setOpen(false);
                      }}
                      className="relative flex h-11 w-full items-center gap-3 rounded-2xl px-3 text-left text-[15px] outline-none transition-colors hover:bg-black/[0.04] focus-visible:bg-black/[0.06] dark:hover:bg-white/[0.06] dark:focus-visible:bg-white/10"
                    >
                      {id === section && (
                        <motion.span
                          layoutId="drawer-active"
                          transition={transition}
                          className="absolute inset-0 rounded-2xl bg-black/[0.06] dark:bg-white/10"
                        />
                      )}
                      <Icon className="relative h-[18px] w-[18px]" strokeWidth={1.5} />
                      <span className={"relative " + (id === section ? "font-medium" : "")}>{label}</span>
                    </button>
                  </li>
                ))}
              </ul>
              <div className="mt-auto flex items-center gap-3 rounded-2xl p-2">
                <span className="grid h-9 w-9 place-items-center rounded-full bg-gradient-to-br from-[#fcd34d] to-[#f97316] text-[12px] font-semibold text-white">
                  ER
                </span>
                <div className="min-w-0">
                  <p className="truncate text-[14px] font-medium leading-tight">Elena Ruiz</p>
                  <p className="text-[12px] text-[#86868b]">Pro plan</p>
                </div>
              </div>
            </motion.div>
          </>
        )}
      </AnimatePresence>
    </div>
  );
}
```

## Prompt for AI assistants

Paste this into ChatGPT, Claude, Cursor, v0 or Lovable to adapt the block to your stack (Tailwind, Vue, Svelte…):

````text
I want to add the “Scale-back drawer” block (Overlays) from AIMRAN Library to my project.

## What it is
A frosted side menu slides in while the page behind it shrinks and dims, like on iOS. Swipe it away to close.

## Chosen settings
- Side: Left (other options: Right)
- Page scale: 90% (from 84 to 96%)
- Page radius: 28px (from 12 to 36px)

## What I need
1. Add it to my project with the same look and animation as the reference code: same sizes, colours, radii, shadows, timings and easing.
2. It is built with React + TypeScript, with framer-motion for the animation and lucide-react for the icons. If my project uses something else (Vue, Svelte, Astro, plain HTML…), translate it without losing any behaviour. If I use Tailwind, turn the styles into classes; if not, keep the CSS in its own file.
3. Expose the settings above as props (or CSS variables) with those values as defaults.
4. Make it work well on phones: nothing that depends only on hover, touch targets of at least 44 px and no overflow on 320 px screens.
5. Make it look right in light and dark mode.
6. Keep it accessible: semantic HTML, roles and aria-* where needed, visible focus and keyboard support.
7. Respect prefers-reduced-motion: with reduced motion, no big animations but the same end result.

## Reference code
```tsx
import { useEffect, useRef, useState, type RefObject } from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { Archive, House, Menu, Star, Users, X } from "lucide-react";

const SECTIONS = [
  { id: "inicio", label: "Home", icon: House },
  { id: "destacadas", label: "Featured", icon: Star },
  { id: "compartidas", label: "Shared", icon: Users },
  { id: "archivo", label: "File", icon: Archive },
];

const NOTE_ART = "radial-gradient(120% 90% at 100% 0%, #ffc49b 0%, transparent 50%), linear-gradient(135deg, #f0527a 0%, #a855f7 60%, #6d5dfc 100%)";

const FOCUSABLE =
  'a[href],button:not([disabled]),input:not([disabled]),select,textarea,[tabindex]:not([tabindex="-1"])';

// Modal accesible: enfoca el primer control (o el marcado con data-autofocus), atrapa Tab, cierra con Esc y devuelve el foco
function useDialog(ref: RefObject<HTMLElement | null>, open: boolean, onClose: () => void) {
  const close = useRef(onClose);
  useEffect(() => {
    close.current = onClose;
  });
  useEffect(() => {
    if (!open) return;
    const previous = document.activeElement as HTMLElement | null;
    const node = ref.current;
    const first =
      node?.querySelector<HTMLElement>("[data-autofocus]") ?? node?.querySelector<HTMLElement>(FOCUSABLE);
    first?.focus({ preventScroll: true });
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        e.preventDefault();
        close.current();
        return;
      }
      if (e.key !== "Tab" || !node) return;
      const items = Array.from(node.querySelectorAll<HTMLElement>(FOCUSABLE));
      if (!items.length) return;
      const firstItem = items[0];
      const lastItem = items[items.length - 1];
      if (!firstItem || !lastItem) return;
      if (!node.contains(document.activeElement)) {
        e.preventDefault();
        firstItem.focus();
      } else if (e.shiftKey && document.activeElement === firstItem) {
        e.preventDefault();
        lastItem.focus();
      } else if (!e.shiftKey && document.activeElement === lastItem) {
        e.preventDefault();
        firstItem.focus();
      }
    };
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("keydown", onKey);
      previous?.focus({ preventScroll: true });
    };
  }, [open, ref]);
}

interface ScaleDrawerProps {
  side?: "left" | "right";
  /** Escala de la página al abrir, en % */
  scale?: number;
  radius?: number;
}

export default function ScaleDrawer({
  side = "left",
  scale = 90,
  radius = 28,
}: ScaleDrawerProps) {
  const [open, setOpen] = useState(false);
  const [section, setSection] = useState("inicio");
  const panel = useRef<HTMLDivElement>(null);
  const reduce = useReducedMotion();
  const transition = reduce ? { duration: 0 } : { type: "spring" as const, stiffness: 300, damping: 32, mass: 0.9 };
  const dir = side === "left" ? 1 : -1;
  const current = SECTIONS.find((s) => s.id === section) ?? SECTIONS[0]!;
  useDialog(panel, open, () => setOpen(false));

  return (
    // h-dvh: la raíz ocupa la ventana y hace de fondo negro; las capas son absolute dentro de ella
    <div className="relative h-dvh w-full overflow-hidden bg-black">
      <motion.div
        animate={
          open
            ? { scale: scale / 100, x: 40 * dir, borderRadius: radius, filter: "brightness(0.7)" }
            : { scale: 1, x: 0, borderRadius: 0, filter: "brightness(1)" }
        }
        transition={transition}
        style={{ transformOrigin: side === "left" ? "100% 50%" : "0% 50%" }}
        className="absolute inset-0 overflow-hidden bg-[#f5f5f7] text-[#1d1d1f] dark:bg-[#161618] dark:text-[#f5f5f7]"
      >
        <div inert={open} aria-hidden={open} className="mx-auto flex h-full max-w-md flex-col px-5 pt-4">
          <div className={"flex items-center justify-between " + (side === "right" ? "flex-row-reverse" : "")}>
            <button
              type="button"
              aria-label="Open menu"
              aria-expanded={open}
              aria-haspopup="dialog"
              onClick={() => setOpen(true)}
              className="grid h-10 w-10 place-items-center rounded-full bg-white shadow-[0_1px_2px_rgba(0,0,0,0.06),0_6px_16px_rgba(0,0,0,0.06)] transition active:scale-95 dark:bg-white/10 dark:shadow-none"
            >
              <Menu className="h-[18px] w-[18px]" strokeWidth={1.5} />
            </button>
            <span className="grid h-9 w-9 place-items-center rounded-full bg-gradient-to-br from-[#fcd34d] to-[#f97316] text-[12px] font-semibold text-white">
              ER
            </span>
          </div>
          <p className="mt-7 text-[13px] text-[#86868b]">Tuesday, September 26</p>
          <AnimatePresence mode="wait" initial={false}>
            <motion.h2
              key={current.id}
              initial={{ opacity: 0, y: 8 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0, y: -8 }}
              transition={{ duration: reduce ? 0 : 0.18 }}
              className="text-[32px] font-semibold leading-tight tracking-[-0.03em]"
            >
              {current.label}
            </motion.h2>
          </AnimatePresence>
          <div
            className="relative mt-5 overflow-hidden rounded-[22px] p-5 text-white shadow-[0_10px_30px_-10px_rgba(168,85,247,0.6)]"
            style={{ background: NOTE_ART }}
          >
            <p className="text-[12px] font-medium text-white/75">Pinned note</p>
            <p className="mt-7 text-[20px] font-semibold leading-tight tracking-[-0.02em]">Lisbon in four days</p>
            <p className="mt-1 text-[13px] text-white/75">Edited 2 min ago</p>
          </div>
          <div className="mt-3 grid grid-cols-2 gap-3">
            <div className="rounded-[20px] bg-white p-4 shadow-[0_1px_2px_rgba(0,0,0,0.04)] dark:bg-white/[0.06] dark:shadow-none">
              <p className="text-[22px] font-semibold leading-none tracking-[-0.03em]">12</p>
              <p className="mt-1.5 text-[13px] text-[#86868b]">Ideas</p>
            </div>
            <div className="rounded-[20px] bg-white p-4 shadow-[0_1px_2px_rgba(0,0,0,0.04)] dark:bg-white/[0.06] dark:shadow-none">
              <p className="text-[22px] font-semibold leading-none tracking-[-0.03em]">8</p>
              <p className="mt-1.5 text-[13px] text-[#86868b]">Recipes</p>
            </div>
          </div>
        </div>
      </motion.div>

      <AnimatePresence>
        {open && (
          <>
            <motion.div key="catcher" aria-hidden className="absolute inset-0" onClick={() => setOpen(false)} />
            <motion.div
              key="drawer"
              ref={panel}
              role="dialog"
              aria-modal="true"
              aria-label="Menu"
              initial={{ x: side === "left" ? "-110%" : "110%" }}
              animate={{ x: 0 }}
              exit={{ x: side === "left" ? "-110%" : "110%" }}
              transition={transition}
              drag="x"
              dragConstraints={{ left: 0, right: 0 }}
              dragElastic={side === "left" ? { left: 0.5, right: 0.05 } : { left: 0.05, right: 0.5 }}
              onDragEnd={(_, info) => {
                if (info.offset.x * dir < -70 || info.velocity.x * dir < -500) setOpen(false);
              }}
              className={"absolute inset-y-3 flex w-[min(78%,300px)] touch-pan-y flex-col rounded-[28px] border border-white/60 bg-white/80 p-3 text-[#1d1d1f] shadow-[0_8px_20px_rgba(0,0,0,0.15),0_30px_70px_rgba(0,0,0,0.35)] backdrop-blur-2xl backdrop-saturate-150 dark:border-white/10 dark:bg-[#232326]/80 dark:text-[#f5f5f7] " + (side === "left" ? "left-3" : "right-3")}
            >
              <div className="flex items-center justify-between px-2 pb-3 pt-1">
                <span className="flex items-center gap-2 text-[15px] font-semibold tracking-[-0.01em]">
                  <span className="h-6 w-6 rounded-[8px]" style={{ background: NOTE_ART }} />
                  Notes
                </span>
                <button
                  type="button"
                  aria-label="Close"
                  onClick={() => setOpen(false)}
                  className="grid h-8 w-8 place-items-center rounded-full bg-black/5 transition hover:bg-black/10 dark:bg-white/10 dark:hover:bg-white/20"
                >
                  <X className="h-4 w-4" strokeWidth={1.5} />
                </button>
              </div>
              <ul className="space-y-0.5">
                {SECTIONS.map(({ id, label, icon: Icon }) => (
                  <li key={id}>
                    <button
                      type="button"
                      aria-current={id === section ? "page" : undefined}
                      onClick={() => {
                        setSection(id);
                        setOpen(false);
                      }}
                      className="relative flex h-11 w-full items-center gap-3 rounded-2xl px-3 text-left text-[15px] outline-none transition-colors hover:bg-black/[0.04] focus-visible:bg-black/[0.06] dark:hover:bg-white/[0.06] dark:focus-visible:bg-white/10"
                    >
                      {id === section && (
                        <motion.span
                          layoutId="drawer-active"
                          transition={transition}
                          className="absolute inset-0 rounded-2xl bg-black/[0.06] dark:bg-white/10"
                        />
                      )}
                      <Icon className="relative h-[18px] w-[18px]" strokeWidth={1.5} />
                      <span className={"relative " + (id === section ? "font-medium" : "")}>{label}</span>
                    </button>
                  </li>
                ))}
              </ul>
              <div className="mt-auto flex items-center gap-3 rounded-2xl p-2">
                <span className="grid h-9 w-9 place-items-center rounded-full bg-gradient-to-br from-[#fcd34d] to-[#f97316] text-[12px] font-semibold text-white">
                  ER
                </span>
                <div className="min-w-0">
                  <p className="truncate text-[14px] font-medium leading-tight">Elena Ruiz</p>
                  <p className="text-[12px] text-[#86868b]">Pro plan</p>
                </div>
              </div>
            </motion.div>
          </>
        )}
      </AnimatePresence>
    </div>
  );
}
```

Give me the complete final code, ready to paste, and tell me in one line where to put it in my project.
````
