Ir al contenido
Evaluar mi proyecto
Componentes · Modales y capasDisponible

Panel lateral de ajustes

Drawer sobre <dialog> que entra desde el lateral, con interruptores, selector y botones de guardar. Material oscuro.

Ajustes

Avisos por correoRecibe un correo con cada novedad
Resumen semanalLos lunes a las 9:00
Tema claroCambia la apariencia de la aplicación
IdiomaSe aplica a toda la interfaz

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

capa-panel.tsx96 líneas
Descargar
"use client";

import { useId, useRef, useState, type CSSProperties } from "react";

/** Panel lateral deslizante (drawer) de ajustes sobre <dialog> nativo: entra desde la derecha, con interruptores, selector y botón de guardar. Material oscuro. React + TypeScript, sin dependencias. */
export type CapaPanelProps = {
  trigger?: string;
  title?: string;
  side?: "derecha" | "izquierda";
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gccpa-root { box-sizing: border-box; display: inline-block; font: 500 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; color: #e9ebf2; }
.gccpa-root *, .gccpa-root *::before, .gccpa-root *::after { box-sizing: border-box; }
.gccpa-root button, .gccpa-root select { font: inherit; }
.gccpa-root :focus-visible { outline: 2px solid #8fb4ff; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .gccpa-dlg, .gccpa-dlg::backdrop { animation: none !important; } }
.gccpa-open { min-height: 44px; padding: 0 18px; border: 1px solid #2f3340; border-radius: 10px; background: #181a21; color: inherit; font-weight: 600; cursor: pointer; transition: background .15s; }
.gccpa-open:hover { background: #20232c; }
.gccpa-dlg { position: fixed; top: 0; bottom: 0; margin: 0; width: min(380px, 100vw); max-width: none; height: 100dvh; max-height: none; padding: 0; border: 0; background: #14161b; color: #e9ebf2; box-shadow: -24px 0 60px -20px rgba(0, 0, 0, .7); animation: gccpa-in .38s cubic-bezier(.16, 1, .3, 1); }
.gccpa-dlg[data-s="derecha"] { right: 0; left: auto; border-left: 1px solid #272a34; }
.gccpa-dlg[data-s="izquierda"] { left: 0; right: auto; border-right: 1px solid #272a34; --d: -100%; }
.gccpa-dlg::backdrop { background: rgba(5, 6, 10, .6); animation: gccpa-fade .3s ease-out; }
.gccpa-wrap { display: grid; grid-template-rows: auto 1fr auto; height: 100%; }
.gccpa-head { display: flex; align-items: center; justify-content: space-between; padding: 18px 20px; border-bottom: 1px solid #23262f; }
.gccpa-title { margin: 0; font-size: 17px; font-weight: 700; }
.gccpa-x { display: grid; place-items: center; width: 36px; height: 36px; border: 0; border-radius: 8px; background: none; color: #9aa1b4; cursor: pointer; }
.gccpa-x:hover { background: #1d2029; color: #fff; }
.gccpa-body { overflow-y: auto; padding: 8px 20px; }
.gccpa-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px 0; border-bottom: 1px solid #1f222a; }
.gccpa-row small { display: block; color: #8b92a5; font-size: 13px; }
.gccpa-sw { position: relative; flex: none; width: 46px; height: 28px; padding: 0; border: 0; border-radius: 999px; background: #2b2f3a; cursor: pointer; transition: background .2s; }
.gccpa-sw::after { content: ""; position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; border-radius: 50%; background: #fff; transition: transform .25s cubic-bezier(.34, 1.4, .64, 1); }
.gccpa-sw[aria-checked="true"] { background: #4b7bff; }
.gccpa-sw[aria-checked="true"]::after { transform: translateX(18px); }
.gccpa-sel { height: 40px; padding: 0 10px; border: 1px solid #2f3340; border-radius: 8px; background: #1b1e26; color: inherit; }
.gccpa-foot { display: flex; gap: 10px; padding: 16px 20px; border-top: 1px solid #23262f; }
.gccpa-b { flex: 1; min-height: 44px; border: 1px solid #2f3340; border-radius: 10px; background: transparent; color: inherit; font-weight: 600; cursor: pointer; }
.gccpa-b:hover { background: #1d2029; }
.gccpa-b[data-p="true"] { border-color: #4b7bff; background: #4b7bff; color: #fff; }
.gccpa-b[data-p="true"]:hover { background: #6a92ff; }
@keyframes gccpa-in { from { transform: translateX(var(--d, 100%)); } }
@keyframes gccpa-fade { from { opacity: 0; } }`;

export function CapaPanel({ trigger = "Abrir ajustes", title = "Ajustes", side = "derecha", className = "", style }: CapaPanelProps) {
  const id = useId();
  const ref = useRef<HTMLDialogElement>(null);
  const [opts, setOpts] = useState({ avisos: true, resumen: false, claro: false });
  const sw = (key: keyof typeof opts, label: string, hint: string) => (
    <div className="gccpa-row">
      <span>
        {label}
        <small>{hint}</small>
      </span>
      <button className="gccpa-sw" type="button" role="switch" aria-checked={opts[key]} aria-label={label} onClick={() => setOpts((prev) => ({ ...prev, [key]: !prev[key] }))} />
    </div>
  );
  return (
    <div className={`gccpa-root ${className}`} style={style}>
      <style>{CSS}</style>
      <button className="gccpa-open" type="button" onClick={() => ref.current?.showModal()}>{trigger}</button>
      <dialog ref={ref} className="gccpa-dlg" data-s={side} aria-labelledby={`${id}-t`} onClick={(event) => event.target === ref.current && ref.current?.close()}>
        <div className="gccpa-wrap">
          <div className="gccpa-head">
            <h2 className="gccpa-title" id={`${id}-t`}>{title}</h2>
            <button className="gccpa-x" type="button" aria-label="Cerrar panel" onClick={() => ref.current?.close()}>
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18" /></svg>
            </button>
          </div>
          <div className="gccpa-body">
            {sw("avisos", "Avisos por correo", "Recibe un correo con cada novedad")}
            {sw("resumen", "Resumen semanal", "Los lunes a las 9:00")}
            {sw("claro", "Tema claro", "Cambia la apariencia de la aplicación")}
            <div className="gccpa-row">
              <span>
                Idioma
                <small>Se aplica a toda la interfaz</small>
              </span>
              <select className="gccpa-sel" aria-label="Idioma" defaultValue="es">
                <option value="es">Español</option>
                <option value="en">English</option>
              </select>
            </div>
          </div>
          <div className="gccpa-foot">
            <button className="gccpa-b" type="button" onClick={() => ref.current?.close()}>Cancelar</button>
            <button className="gccpa-b" data-p="true" type="button" onClick={() => ref.current?.close()}>Guardar</button>
          </div>
        </div>
      </dialog>
    </div>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/capa-panel

Ejemplo de uso de Panel lateral de ajustes

Después de copiar capa-panel.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { CapaPanel } from "./capa-panel";

export default function Ejemplo() {
  return <CapaPanel side="derecha" trigger="Ejemplo" />;
}

Propiedades de CapaPanel

CapaPanel acepta 5 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
trigger?stringOpcional.
title?stringOpcional.
side?"derecha" | "izquierda"Opcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Variantes disponibles

  • side: derecha, izquierda

Personaliza Panel lateral de ajustes con variables CSS

Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.

VariableValor por defecto
--d100%

Accesibilidad y rendimiento

El archivo tiene 96 líneas (6 KB) y define 2 animaciones con @keyframes. Esto es lo que hace Panel lateral de ajustes 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).
  • Declara roles ARIA para lectores de pantalla: switch.
  • Usa el elemento <dialog> nativo, con foco y cierre con Escape gestionados por el navegador.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)