Lista de deslizar para borrar
Arrastra una fila para revelar la acción roja; pasado el umbral la fila se colapsa. Con botón de borrar para teclado.
- Cotización #204
- Reunión de arranque
- Factura de marzo
- Revisión de accesos
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useRef, useState, type CSSProperties, type PointerEvent } from "react";
/** Lista con deslizar para borrar: arrastra una fila para revelar la acción roja; suelta pasado el umbral y la fila se colapsa. Cada fila tiene también un botón de borrar para teclado. React + TypeScript, sin dependencias. */
export type DeslizarBorrarItem = { id: string; title: string; meta: string };
export type DeslizarBorrarProps = {
items?: DeslizarBorrarItem[];
onDelete?: (id: string) => void;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcmdb-root { box-sizing: border-box; width: 100%; max-width: 380px; font: 500 15px/1.3 -apple-system, "Helvetica Neue", ui-sans-serif, system-ui, sans-serif; color: #1c1c1e; }
.gcmdb-root *, .gcmdb-root *::before, .gcmdb-root *::after { box-sizing: border-box; }
.gcmdb-root button { font: inherit; }
.gcmdb-root :focus-visible { outline: 3px solid #0a84ff; outline-offset: -3px; }
@media (prefers-reduced-motion: reduce) { .gcmdb-root * { transition-duration: .01ms !important; } }
.gcmdb-list { margin: 0; padding: 0; list-style: none; overflow: hidden; border-radius: 16px; background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, .08), 0 10px 24px -14px rgba(0, 0, 0, .25); }
.gcmdb-wrap { display: grid; grid-template-rows: 1fr; transition: grid-template-rows .35s cubic-bezier(.16, 1, .3, 1), opacity .3s; }
.gcmdb-wrap[data-gone="true"] { grid-template-rows: 0fr; opacity: 0; }
.gcmdb-wrap > div { overflow: hidden; }
.gcmdb-row { position: relative; background: #ff3b30; border-top: 1px solid #ececf0; }
.gcmdb-wrap:first-child .gcmdb-row { border-top: 0; }
.gcmdb-del { position: absolute; right: 0; top: 0; bottom: 0; width: 96px; display: grid; place-items: center; align-content: center; gap: 2px; border: 0; background: transparent; color: #fff; font-size: 13px; font-weight: 600; cursor: pointer; }
.gcmdb-front { position: relative; display: flex; align-items: center; gap: 14px; padding: 14px 16px; background: #fff; touch-action: pan-y; user-select: none; cursor: grab; transition: transform .3s cubic-bezier(.16, 1, .3, 1); }
.gcmdb-front[data-drag="true"] { transition: none; cursor: grabbing; }
.gcmdb-avatar { flex: none; display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; background: #e8f0ff; color: #2456c8; font-weight: 700; }
.gcmdb-title { display: block; font-weight: 600; }
.gcmdb-meta { display: block; margin-top: 2px; font-size: 13px; color: #6c6c70; }`;
const DEFAULT_ITEMS: DeslizarBorrarItem[] = [
{ id: "1", title: "Cotización #204", meta: "Enviada hoy · 10:42" },
{ id: "2", title: "Reunión de arranque", meta: "Mañana · 9:00" },
{ id: "3", title: "Factura de marzo", meta: "Vence en 5 días" },
{ id: "4", title: "Revisión de accesos", meta: "Pendiente" },
];
function Row({ item, onDelete }: { item: DeslizarBorrarItem; onDelete: (id: string) => void }) {
const [x, setX] = useState(0);
const [drag, setDrag] = useState(false);
const [gone, setGone] = useState(false);
const start = useRef<number | null>(null);
const OPEN = -96;
function remove() {
setX(-420);
setGone(true);
window.setTimeout(() => onDelete(item.id), 340);
}
function down(event: PointerEvent<HTMLDivElement>) {
start.current = event.clientX - x;
event.currentTarget.setPointerCapture(event.pointerId);
setDrag(true);
}
function move(event: PointerEvent<HTMLDivElement>) {
if (start.current === null) return;
const next = Math.max(-300, Math.min(0, event.clientX - start.current));
setX(next < OPEN ? OPEN + (next - OPEN) * 0.5 : next);
}
function up() {
if (start.current === null) return;
start.current = null;
setDrag(false);
if (x < -200) remove();
else setX(x < OPEN / 2 ? OPEN : 0);
}
return (
<li className="gcmdb-wrap" data-gone={gone}>
<div>
<div className="gcmdb-row">
<button className="gcmdb-del" type="button" aria-label={`Borrar ${item.title}`} tabIndex={x === OPEN ? 0 : 0} onClick={remove}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3" />
</svg>
Borrar
</button>
<div className="gcmdb-front" data-drag={drag} style={{ transform: `translateX(${x}px)` }} onPointerDown={down} onPointerMove={move} onPointerUp={up} onPointerCancel={up}>
<span className="gcmdb-avatar" aria-hidden="true">
{item.title[0]}
</span>
<span>
<span className="gcmdb-title">{item.title}</span>
<span className="gcmdb-meta">{item.meta}</span>
</span>
</div>
</div>
</div>
</li>
);
}
export function DeslizarBorrar({ items = DEFAULT_ITEMS, onDelete, className = "", style }: DeslizarBorrarProps) {
const [list, setList] = useState(items);
return (
<div className={`gcmdb-root ${className}`} style={style}>
<style>{CSS}</style>
<ul className="gcmdb-list" aria-label="Pendientes">
{list.map((item) => (
<Row key={item.id} item={item} onDelete={(id) => { setList((prev) => prev.filter((value) => value.id !== id)); onDelete?.(id); }} />
))}
</ul>
{list.length === 0 ? (
<p style={{ textAlign: "center", color: "#6c6c70", marginTop: 16 }}>
Sin pendientes.{" "}
<button type="button" style={{ border: 0, background: "none", color: "#0a84ff", cursor: "pointer", font: "inherit" }} onClick={() => setList(items)}>
Restaurar lista
</button>
</p>
) : null}
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/deslizar-borrarEjemplo de uso de Lista de deslizar para borrar
Después de copiar deslizar-borrar.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { DeslizarBorrar } from "./deslizar-borrar";
export default function Ejemplo() {
return <DeslizarBorrar />;
}Propiedades de DeslizarBorrar
DeslizarBorrar acepta 4 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
items? | DeslizarBorrarItem[] | Opcional. |
onDelete? | (id: string) => void | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 117 líneas (6 KB) y no necesita @keyframes. Esto es lo que hace Lista de deslizar para borrar por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Reacciona al puntero con eventos pointer (ratón, lápiz y táctil).
- Es un componente de cliente de Next.js: incluye la directiva "use client".