React + MotionOverlays

Hoja inferior deslizable

Un panel que sube desde abajo con un asa y se cierra al arrastrarlo hacia abajo o tocar fuera.

Ajustes

240px

Código

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"
      >
        Abrir sheet
      </button>
      <AnimatePresence>
        {open && (
          <>
            <motion.button
              type="button"
              aria-label="Cerrar"
              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">Tu próxima escapada</h3>
              <p className="mt-1 text-sm text-[#86868b]">Guarda este alojamiento para más tarde.</p>
              <button
                type="button"
                className="mt-5 w-full rounded-full bg-[#0071e3] py-3 text-sm font-medium text-white"
              >
                Guardar
              </button>
            </motion.div>
          </>
        )}
      </AnimatePresence>
    </div>
  );
}

Prompt para IA

Pégalo en ChatGPT, Claude o Cursor y adaptará el bloque a tu proyecto con estos ajustes, aunque no uses React.

Más en Overlays

Dónde usarlo

En el móvil, una hoja inferior es más amable que un modal centrado: se abre donde ya está el pulgar y se aparta con un gesto. Úsala para «Guardar este alojamiento» en una app de viajes, elegir talla en una tienda o compartir un artículo. Este bottom sheet en React se cierra arrastrando, como en las apps nativas.

Deja el contenido lo bastante corto para que quepa sin scroll dentro de la hoja; si hace falta un formulario largo, abre una página completa. Ajusta la altura para que la página de detrás siga asomando, así se entiende que se puede volver.