Ir al contenido
Evaluar mi proyecto
Componentes · NavegaciónDisponible

Barra lateral colapsable

Se contrae a un riel de iconos con etiquetas flotantes; secciones, elemento activo e insignias.

Código completo

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

nav-lateral.tsx92 líneas
Descargar
"use client";

import { useState, type CSSProperties, type ReactNode } from "react";

/** Barra lateral que se contrae a un riel de iconos con etiquetas flotantes. Secciones, elemento activo y botón de colapsar con aria-expanded. Estilo minimalista de aplicación. React + TypeScript, sin dependencias. */
export type NavLateralItem = { id: string; label: string; icon: ReactNode; badge?: string };
export type NavLateralSeccion = { title?: string; items: NavLateralItem[] };

export type NavLateralProps = {
  sections?: NavLateralSeccion[];
  defaultCollapsed?: boolean;
  defaultActive?: string;
  onSelect?: (id: string) => void;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcnla-root { box-sizing: border-box; display: flex; flex-direction: column; width: var(--w, 248px); max-width: 100%; height: 440px; padding: 12px; border: 1px solid #e4e4e7; border-radius: 16px; background: #fff; color: #18181b; font: 500 14px/1.2 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; transition: width .35s cubic-bezier(.16, 1, .3, 1); }
.gcnla-root[data-c="true"] { --w: 72px; }
.gcnla-root *, .gcnla-root *::before, .gcnla-root *::after { box-sizing: border-box; }
.gcnla-root button { font: inherit; -webkit-tap-highlight-color: transparent; }
.gcnla-root :focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .gcnla-root, .gcnla-root * { transition-duration: .01ms !important; } }
.gcnla-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; height: 44px; margin-bottom: 10px; padding-left: 6px; overflow: hidden; }
.gcnla-logo { display: flex; align-items: center; gap: 10px; font-weight: 700; white-space: nowrap; }
.gcnla-logo i { flex: none; width: 28px; height: 28px; border-radius: 8px; background: #18181b; }
.gcnla-root[data-c="true"] .gcnla-logo span, .gcnla-root[data-c="true"] .gcnla-sec { display: none; }
.gcnla-toggle { flex: none; display: grid; place-items: center; width: 36px; height: 36px; border: 0; border-radius: 8px; background: none; color: #71717a; cursor: pointer; }
.gcnla-toggle:hover { background: #f4f4f5; color: #18181b; }
.gcnla-root[data-c="true"] .gcnla-top { justify-content: center; padding: 0; flex-direction: column; height: auto; gap: 10px; }
.gcnla-root[data-c="true"] .gcnla-toggle svg { transform: rotate(180deg); }
.gcnla-nav { display: grid; gap: 4px; overflow-y: auto; scrollbar-width: none; }
.gcnla-sec { margin: 12px 10px 4px; font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: #a1a1aa; white-space: nowrap; }
.gcnla-item { position: relative; display: flex; align-items: center; gap: 12px; height: 42px; padding: 0 11px; border: 0; border-radius: 10px; background: none; color: #52525b; text-align: left; cursor: pointer; white-space: nowrap; transition: background .15s, color .15s; }
.gcnla-item:hover { background: #f4f4f5; color: #18181b; }
.gcnla-item[aria-current="page"] { background: #18181b; color: #fff; }
.gcnla-item svg { flex: none; }
.gcnla-label { flex: 1; overflow: hidden; }
.gcnla-badge { padding: 2px 7px; border-radius: 99px; background: #e4e4e7; color: #3f3f46; font-size: 11px; font-weight: 600; }
.gcnla-item[aria-current="page"] .gcnla-badge { background: rgba(255, 255, 255, .2); color: #fff; }
.gcnla-root[data-c="true"] .gcnla-label, .gcnla-root[data-c="true"] .gcnla-badge { display: none; }
.gcnla-tip { position: absolute; left: calc(100% + 12px); top: 50%; z-index: 4; padding: 7px 10px; border-radius: 8px; background: #18181b; color: #fff; font-size: 12px; white-space: nowrap; opacity: 0; pointer-events: none; transform: translate(-4px, -50%); transition: opacity .15s, transform .15s; }
.gcnla-root[data-c="true"] .gcnla-item:hover .gcnla-tip, .gcnla-root[data-c="true"] .gcnla-item:focus-visible .gcnla-tip { opacity: 1; transform: translate(0, -50%); }
.gcnla-root[data-c="false"] .gcnla-tip { display: none; }`;

const I = ({ d }: { d: string }) => (
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <path d={d} />
  </svg>
);

const DEFAULT: NavLateralSeccion[] = [
  { items: [{ id: "panel", label: "Panel", icon: <I d="M4 4h7v9H4zM13 4h7v5h-7zM13 11h7v9h-7zM4 15h7v5H4z" /> }, { id: "proyectos", label: "Proyectos", icon: <I d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />, badge: "12" }, { id: "tareas", label: "Tareas", icon: <I d="m5 12.5 4.5 4.5L19 7.5" />, badge: "5" }] },
  { title: "Equipo", items: [{ id: "personas", label: "Personas", icon: <I d="M9 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM2.5 20c.8-3.2 3.2-5 6.5-5s5.7 1.8 6.5 5M16 4.5a3.5 3.5 0 0 1 0 6.5M18 15c2 .5 3.2 2 3.5 5" /> }, { id: "reportes", label: "Reportes", icon: <I d="M5 20V10M12 20V4M19 20v-7" /> }] },
  { title: "Cuenta", items: [{ id: "ajustes", label: "Ajustes", icon: <I d="M4 7h9M17 7h3M4 17h3M11 17h9M15 5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM9 15a2 2 0 1 0 0 4 2 2 0 0 0 0-4z" /> }] },
];

export function NavLateral({ sections = DEFAULT, defaultCollapsed = false, defaultActive, onSelect, className = "", style }: NavLateralProps) {
  const [collapsed, setCollapsed] = useState(defaultCollapsed);
  const [active, setActive] = useState(defaultActive ?? sections[0].items[0].id);
  return (
    <aside className={`gcnla-root ${className}`} style={style} data-c={collapsed}>
      <style>{CSS}</style>
      <div className="gcnla-top">
        <span className="gcnla-logo">
          <i aria-hidden="true" />
          <span>Estudio</span>
        </span>
        <button className="gcnla-toggle" type="button" aria-expanded={!collapsed} aria-label={collapsed ? "Expandir barra lateral" : "Contraer barra lateral"} onClick={() => setCollapsed((value) => !value)}>
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="m14 7-5 5 5 5" /></svg>
        </button>
      </div>
      <nav className="gcnla-nav" aria-label="Aplicación">
        {sections.map((section, index) => (
          <div key={index} style={{ display: "contents" }}>
            {section.title ? <p className="gcnla-sec">{section.title}</p> : null}
            {section.items.map((item) => (
              <button key={item.id} className="gcnla-item" type="button" aria-current={active === item.id ? "page" : undefined} aria-label={collapsed ? item.label : undefined} onClick={() => { setActive(item.id); onSelect?.(item.id); }}>
                {item.icon}
                <span className="gcnla-label">{item.label}</span>
                {item.badge ? <span className="gcnla-badge">{item.badge}</span> : null}
                <span className="gcnla-tip" aria-hidden="true">{item.label}</span>
              </button>
            ))}
          </div>
        ))}
      </nav>
    </aside>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

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

Ejemplo de uso de Barra lateral colapsable

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

ejemplo.tsx5 líneas
import { NavLateral } from "./nav-lateral";

export default function Ejemplo() {
  return <NavLateral defaultActive="Ejemplo" defaultCollapsed />;
}

Propiedades de NavLateral

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

PropiedadTipoDescripción
sections?NavLateralSeccion[]Opcional.
defaultCollapsed?booleanOpcional.
defaultActive?stringOpcional.
onSelect?(id: string) => voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Personaliza Barra lateral colapsable 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
--w248px

Accesibilidad y rendimiento

El archivo tiene 92 líneas (7 KB) y no necesita @keyframes. Esto es lo que hace Barra lateral colapsable 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-*.
  • 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)