# Bottom sheet

> A panel that slides up from the bottom with a grab handle, and closes when you drag it down or tap outside.

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

## Where to use it

On phones, a bottom sheet is kinder than a centered modal: it opens where the thumb already is and can be flicked away. Use it for “Save this place” on a travel app, picking a size in a shop or sharing a blog post. This React bottom sheet with drag to close behaves the way people expect from native apps.

Keep the content short enough to fit without scrolling inside the sheet; if it needs a long form, open a full page instead. Set the height so the page behind stays partly visible, which reminds people they can go back.

## Code

### React (Motion)

```tsx
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";

const spring = { type: "spring" as const, stiffness: 300, damping: 30 };

interface BottomSheetProps {
  height?: number;
}

export default function BottomSheet({ height = 240 }: BottomSheetProps) {
  const [open, setOpen] = useState(false);

  return (
    <div className="relative flex h-64 w-full flex-col items-center justify-center overflow-hidden rounded-2xl bg-black/5 text-[#1d1d1f] dark:text-[#f5f5f7] dark:bg-white/10 sm:h-72">
      {/* Relleno: da a la caja 320 px de ancho natural sin impedir que encoja en pantallas estrechas */}
      <svg aria-hidden className="h-0 w-80 max-w-full" />
      <button
        type="button"
        onClick={() => setOpen(true)}
        className="rounded-full bg-[#0071e3] px-5 py-2.5 text-sm font-medium text-white"
      >
        Open sheet
      </button>
      <AnimatePresence>
        {open && (
          <>
            <motion.button
              type="button"
              aria-label="Close"
              className="absolute inset-0 bg-[#1d1d1f]/10 dark:bg-[#f5f5f7]/10"
              onClick={() => setOpen(false)}
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
            />
            <motion.div
              drag="y"
              dragConstraints={{ top: 0, bottom: 100 }}
              onDragEnd={(_, info) => {
                if (info.offset.y > 60) setOpen(false);
              }}
              initial={{ y: "100%" }}
              animate={{ y: 0 }}
              exit={{ y: "100%" }}
              transition={spring}
              className="absolute inset-x-0 bottom-0 rounded-t-[28px] bg-white dark:bg-[#1c1c1e] p-5 shadow-[0_4px_12px_rgba(0,0,0,0.04),0_24px_60px_rgba(0,0,0,0.1)]"
              style={{ minHeight: `min(${height}px, 100%)` }}
            >
              <div className="mx-auto mb-5 h-1.5 w-10 rounded-full bg-black/5 dark:bg-white/10" />
              <h3 className="text-lg font-semibold">Your next getaway</h3>
              <p className="mt-1 text-sm text-[#86868b]">Save this stay for later.</p>
              <button
                type="button"
                className="mt-5 w-full rounded-full bg-[#0071e3] py-3 text-sm font-medium text-white"
              >
                Save
              </button>
            </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 “Bottom sheet” block (Overlays) from AIMRAN Library to my project.

## What it is
A panel that slides up from the bottom with a grab handle, and closes when you drag it down or tap outside.

## Chosen settings
- Height: 240px (from 180 to 320px)

## 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 { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";

const spring = { type: "spring" as const, stiffness: 300, damping: 30 };

interface BottomSheetProps {
  height?: number;
}

export default function BottomSheet({ height = 240 }: BottomSheetProps) {
  const [open, setOpen] = useState(false);

  return (
    <div className="relative flex h-64 w-full flex-col items-center justify-center overflow-hidden rounded-2xl bg-black/5 text-[#1d1d1f] dark:text-[#f5f5f7] dark:bg-white/10 sm:h-72">
      {/* Relleno: da a la caja 320 px de ancho natural sin impedir que encoja en pantallas estrechas */}
      <svg aria-hidden className="h-0 w-80 max-w-full" />
      <button
        type="button"
        onClick={() => setOpen(true)}
        className="rounded-full bg-[#0071e3] px-5 py-2.5 text-sm font-medium text-white"
      >
        Open sheet
      </button>
      <AnimatePresence>
        {open && (
          <>
            <motion.button
              type="button"
              aria-label="Close"
              className="absolute inset-0 bg-[#1d1d1f]/10 dark:bg-[#f5f5f7]/10"
              onClick={() => setOpen(false)}
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
            />
            <motion.div
              drag="y"
              dragConstraints={{ top: 0, bottom: 100 }}
              onDragEnd={(_, info) => {
                if (info.offset.y > 60) setOpen(false);
              }}
              initial={{ y: "100%" }}
              animate={{ y: 0 }}
              exit={{ y: "100%" }}
              transition={spring}
              className="absolute inset-x-0 bottom-0 rounded-t-[28px] bg-white dark:bg-[#1c1c1e] p-5 shadow-[0_4px_12px_rgba(0,0,0,0.04),0_24px_60px_rgba(0,0,0,0.1)]"
              style={{ minHeight: `min(${height}px, 100%)` }}
            >
              <div className="mx-auto mb-5 h-1.5 w-10 rounded-full bg-black/5 dark:bg-white/10" />
              <h3 className="text-lg font-semibold">Your next getaway</h3>
              <p className="mt-1 text-sm text-[#86868b]">Save this stay for later.</p>
              <button
                type="button"
                className="mt-5 w-full rounded-full bg-[#0071e3] py-3 text-sm font-medium text-white"
              >
                Save
              </button>
            </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.
````
