Ir al contenido
Evaluar mi proyecto
Componentes · NavegaciónDisponible

Riel de navegación Material 3

FAB arriba y destinos con indicador en píldora que crece desde el centro, según Material 3.

Código completo

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

nav-riel.tsx71 líneas
Descargar
"use client";

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

/** Riel de navegación Material Design 3: botón de acción flotante arriba, destinos con indicador en píldora que crece desde el centro y etiquetas. React + TypeScript, sin dependencias. */
export type NavRielItem = { id: string; label: string; icon: ReactNode; badge?: boolean };

export type NavRielProps = {
  items?: NavRielItem[];
  defaultValue?: string;
  onChange?: (id: string) => void;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcnrl-root { box-sizing: border-box; display: flex; flex-direction: column; align-items: center; gap: 20px; width: 80px; padding: 44px 0 20px; border-radius: 0 16px 16px 0; background: #f7f2fa; color: #49454f; font: 500 12px/16px Roboto, "Google Sans", ui-sans-serif, system-ui, sans-serif; letter-spacing: .5px; }
.gcnrl-root *, .gcnrl-root *::before, .gcnrl-root *::after { box-sizing: border-box; }
.gcnrl-root button { font: inherit; letter-spacing: inherit; -webkit-tap-highlight-color: transparent; }
.gcnrl-root :focus-visible { outline: 3px solid #1d1b20; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .gcnrl-root * { transition-duration: .01ms !important; } }
.gcnrl-fab { display: grid; place-items: center; width: 56px; height: 56px; border: 0; border-radius: 16px; background: #eaddff; color: #21005d; cursor: pointer; box-shadow: 0 1px 3px 1px rgba(0, 0, 0, .15), 0 1px 2px rgba(0, 0, 0, .3); transition: box-shadow .2s, border-radius .2s; }
.gcnrl-fab:hover { box-shadow: 0 1px 3px 1px rgba(0, 0, 0, .15), 0 2px 6px 2px rgba(0, 0, 0, .15); }
.gcnrl-fab:active { border-radius: 12px; }
.gcnrl-list { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; }
.gcnrl-dest { display: grid; justify-items: center; gap: 4px; width: 80px; border: 0; background: none; color: inherit; cursor: pointer; }
.gcnrl-ind { position: relative; display: grid; place-items: center; width: 56px; height: 32px; border-radius: 16px; transition: color .2s; }
.gcnrl-ind::before { content: ""; position: absolute; inset: 0; border-radius: 16px; background: #e8def8; transform: scaleX(0); transition: transform .3s cubic-bezier(.2, 0, 0, 1); }
.gcnrl-dest:hover .gcnrl-ind::after { content: ""; position: absolute; inset: 0; border-radius: 16px; background: rgba(29, 27, 32, .08); }
.gcnrl-dest[aria-selected="true"] .gcnrl-ind::before { transform: scaleX(1); }
.gcnrl-dest[aria-selected="true"] { color: #1d1b20; }
.gcnrl-dest[aria-selected="true"] .gcnrl-ind { color: #1d192b; }
.gcnrl-ind svg { position: relative; z-index: 1; width: 24px; height: 24px; }
.gcnrl-dot { position: absolute; z-index: 2; top: 3px; right: 13px; width: 8px; height: 8px; border-radius: 50%; background: #b3261e; }`;

const I = ({ d }: { d: string }) => (
  <svg 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: NavRielItem[] = [
  { id: "inicio", label: "Inicio", icon: <I d="m3 11 9-7 9 7v9a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z" /> },
  { id: "mensajes", label: "Mensajes", icon: <I d="M4 5h16v11H9l-5 4z" />, badge: true },
  { id: "archivos", label: "Archivos", 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" /> },
  { 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 NavRiel({ items = DEFAULT, defaultValue, onChange, className = "", style }: NavRielProps) {
  const [active, setActive] = useState(defaultValue ?? items[0].id);
  return (
    <nav className={`gcnrl-root ${className}`} style={style} aria-label="Principal">
      <style>{CSS}</style>
      <button className="gcnrl-fab" type="button" aria-label="Redactar">
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M4 20h4L19 9a2.800 2.800 0 0 0-4-4L4 16zM13.500 6.500l4 4" /></svg>
      </button>
      <div className="gcnrl-list" role="tablist" aria-orientation="vertical">
        {items.map((item) => (
          <button key={item.id} className="gcnrl-dest" type="button" role="tab" aria-selected={active === item.id} onClick={() => { setActive(item.id); onChange?.(item.id); }}>
            <span className="gcnrl-ind">
              {item.icon}
              {item.badge ? <i className="gcnrl-dot" aria-label="Novedades" /> : null}
            </span>
            {item.label}
          </button>
        ))}
      </div>
    </nav>
  );
}

Ú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-riel

Ejemplo de uso de Riel de navegación Material 3

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

ejemplo.tsx5 líneas
import { NavRiel } from "./nav-riel";

export default function Ejemplo() {
  return <NavRiel defaultValue="Ejemplo" />;
}

Propiedades de NavRiel

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

PropiedadTipoDescripción
items?NavRielItem[]Opcional.
defaultValue?stringOpcional.
onChange?(id: string) => voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Accesibilidad y rendimiento

El archivo tiene 71 líneas (5 KB) y no necesita @keyframes. Esto es lo que hace Riel de navegación Material 3 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: tablist, tab.
  • 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)