Estilos activos con NavLink

Introducción

En esta clase vamos a mejorar la experiencia de usuario de nuestra navegación añadiendo estilos activos: indicadores visuales que muestran en qué página está el usuario actualmente.

Hasta ahora hemos implementado navegación funcional con React Router, pero nos falta un detalle importante de UX: el usuario no tiene feedback visual de dónde está dentro de la aplicación.

Vamos a solucionar esto usando NavLink, un componente especial de React Router que nos permite aplicar estilos dinámicos según si la ruta está activa o no.

Punto de partida: el problema en el header

Partimos del header de la aplicación, donde ya tenemos un enlace a la página de Empleos dentro de la navegación.

Ahora mismo hay varios problemas de experiencia de usuario:

  • Cuando el usuario está en la página de Empleos, el enlace del header sigue siendo clicable
  • El usuario no tiene ninguna pista visual de que ya está en esa página
  • A nivel de UX, no es ideal dejar que el usuario pulse una y otra vez en la misma ruta sin ningún feedback

Por qué necesitamos estilos activos

Si ya estoy en /jobs (Empleos), volver a navegar a /jobs no tiene mucho sentido.

Es más útil que el usuario vea claramente que ya está en esa sección.

La mayoría de menús de navegación marcan la sección actual con:

  • Color de texto distinto
  • Subrayado o fondo especial
  • Enlace deshabilitado o con menor opacidad

Beneficios de los estilos activos

  • Mejor orientación dentro de la app
  • Menos clics innecesarios del usuario
  • Mejor experiencia en teclado y lectores de pantalla
  • Feedback visual claro de la navegación

Para conseguir estos estilos activos, React Router nos ofrece un componente especial: NavLink.

  • Es muy parecido a Link
  • Usa la prop to en lugar de href
  • Añade una feature clave: sabe si la ruta está activa y nos permite reaccionar a ello

Import típico

import { NavLink } from 'react-router'

Recordatorio importante

Los componentes de React Router no aceptan href.

Siempre hay que usar la prop to para indicar la ruta de destino.

En el header teníamos algo similar a esto:

import { Link } from 'react-router'

export function Header() {
  return (
    <header className="header">
      <nav className="header-nav">
        <Link to="/jobs" className="header-nav-link">
          Empleos
        </Link>
      </nav>
    </header>
  )
}

Cambiamos Link por NavLink para el enlace de Empleos:

import { NavLink } from 'react-router'

export function Header() {
  return (
    <header className="header">
      <nav className="header-nav">
        <NavLink to="/jobs" className="header-nav-link">
          Empleos
        </NavLink>
      </nav>
    </header>
  )
}

Así ya tenemos el componente correcto para poder trabajar con el estado de “activo”.

className como función: detectando si la ruta está activa

Lo realmente interesante de NavLink es que la prop className puede ser:

  • Un string (como siempre)
  • O una función que recibe un objeto con información del enlace, entre ella isActive

Cómo funciona

Esto nos permite saber si la ruta del NavLink coincide con la ruta actual y, en función de ello, devolver unas clases u otras.

Ejemplo base

<NavLink
  to="/jobs"
  className={({ isActive }) => (isActive ? 'header-nav-link navLinkActive' : 'header-nav-link')}
>
  Empleos
</NavLink>

Aquí:

  • isActive será true cuando la URL actual coincida con /jobs
  • En ese caso añadimos la clase extra navLinkActive
  • Si no está activa, solo dejamos la clase normal header-nav-link

Esto nos da control total desde CSS.

Cuando inspeccionamos el DOM en las devtools, vemos que React Router hace varias cosas por nosotros de forma automática:

  • Añade la clase que hemos definido, por ejemplo navLinkActive
  • Añade atributos de accesibilidad como aria-current="page" cuando el enlace está activo

Ejemplo de HTML resultante

Cuando la ruta está activa:

<a class="header-nav-link navLinkActive" aria-current="page" href="/jobs"> Empleos </a>

Ese aria-current="page" es muy útil para lectores de pantalla, ya que indica que ese enlace representa la página actual.

Estilos básicos para resaltar la ruta activa

Ahora que tenemos la clase navLinkActive aplicándose dinámicamente, podemos definir estilos específicos para la ruta activa.

Primera prueba con color

En tu archivo de estilos globales (por ejemplo index.css):

.header-nav-link.navLinkActive {
  color: yellow;
}

Con esto ya se ve claramente:

  • Cuando estás en la página de Empleos, el enlace aparece en amarillo
  • Si navegas a otra página (por ejemplo la home), el color vuelve al normal

Es la forma más directa de explicar visualmente el concepto de estilo activo.

Deshabilitar el enlace cuando ya estás en esa ruta

Siguiente mejora: si ya estoy en /jobs, ¿tiene sentido que pueda volver a hacer clic en “Empleos”?

Podemos usar CSS para:

  • Evitar que el usuario pueda hacer clic
  • Indicar visualmente que es el elemento actual (por ejemplo con menor opacidad)

Estilos completos

.header-nav-link.navLinkActive {
  color: yellow;
  pointer-events: none; /* Desactiva el clic */
  opacity: 0.6; /* Se ve un poco más apagado */
  user-select: none; /* Evita seleccionar el texto */
}

Con esto conseguimos:

  • El enlace de la ruta actual ya no es clicable
  • Visualmente se distingue del resto
  • Se refuerza la sensación de “ya estás aquí”

Variaciones de estilos

Puedes complementar con más estilos según tu diseño:

.header-nav-link.navLinkActive {
  font-weight: 600;
  text-decoration: underline;
  border-bottom: 2px solid currentColor;
}

La idea es que NavLink solo te da la información, y tú decides cómo usarla en estilos.

Experimentando con los cambios

Durante la implementación puedes probar varios cambios para ver cómo funciona:

  • Ver cómo cambia el HTML cuando pasas de la home a la página de empleos
  • Observar cómo aparece y desaparece la clase activa
  • Ver cómo se añade y se quita el atributo aria-current
  • Probar estilos llamativos (como el color amarillo) para que el cambio sea súper evidente
  • Demostrar cómo, con pointer-events: none, deja de poder hacerse clic en el enlace de la página actual

Todo esto ayuda a afianzar la idea de que los estilos activos son dinámicos y dependen del estado de la ruta.

Si has seguido el patrón de abstracción que vimos en clases anteriores con Link y useRouter, puedes aplicar lo mismo con NavLink:

// components/NavLink.jsx
import { NavLink as RRNavLink } from 'react-router'

export function NavLink(props) {
  return <RRNavLink {...props} />
}

Así:

  • Toda tu app usa tu componente <NavLink>
  • Si mañana quieres cambiar de librería de routing, solo cambias este archivo
  • No tienes que revisar todos los componentes de navegación uno por uno

Este patrón es especialmente útil si quieres:

  • Añadir lógica común a todos los enlaces de navegación
  • Customizar el comportamiento por defecto
  • Mantener una API consistente en toda tu aplicación
  • Para enlaces normales que no necesitan indicar estado activo
  • Botones o enlaces dentro del contenido
  • Enlaces que no forman parte de la navegación principal
  • Menús de navegación donde quieres mostrar la página actual
  • Tabs o pestañas donde el usuario necesita ver cuál está activa
  • Cualquier lista de enlaces donde importa indicar “estás aquí”

Tabla comparativa

CaracterísticaLinkNavLink
Navegación básica
Prop to
Detecta ruta activa
className función
aria-current auto
Uso en menúsPosibleRecomendado
Estilos activosManualAutomático con callback

Usar href en lugar de to

{
  /* ❌ Incorrecto */
}
;<NavLink href="/jobs">Empleos</NavLink>

{
  /* ✅ Correcto */
}
;<NavLink to="/jobs">Empleos</NavLink>

No usar la función en className

{
  /* ❌ Incorrecto - no aprovecha isActive */
}
;<NavLink to="/jobs" className="header-nav-link">
  Empleos
</NavLink>

{
  /* ✅ Correcto */
}
;<NavLink
  to="/jobs"
  className={({ isActive }) => (isActive ? 'header-nav-link navLinkActive' : 'header-nav-link')}
>
  Empleos
</NavLink>

Olvidar el selector de especificidad en CSS

/* ❌ Incorrecto - puede no tener suficiente especificidad */
.navLinkActive {
  color: yellow;
}

/* ✅ Correcto - mayor especificidad */
.header-nav-link.navLinkActive {
  color: yellow;
}

Casos de uso avanzados

Si tienes subrutas, NavLink también puede detectarlas:

<NavLink
  to="/jobs"
  className={({ isActive }) => (isActive ? 'header-nav-link navLinkActive' : 'header-nav-link')}
>
  Empleos
</NavLink>

Por defecto, /jobs estará activo tanto en /jobs como en /jobs/123.

Desactivar matching de subrutas

Si quieres que solo coincida exactamente:

<NavLink
  to="/jobs"
  end
  className={({ isActive }) => (isActive ? 'header-nav-link navLinkActive' : 'header-nav-link')}
>
  Empleos
</NavLink>

La prop end hace que solo coincida exactamente con /jobs, no con /jobs/123.

Usar style en lugar de className

También puedes usar style como función:

<NavLink
  to="/jobs"
  style={({ isActive }) => ({
    color: isActive ? 'yellow' : 'white',
    fontWeight: isActive ? 'bold' : 'normal',
  })}
>
  Empleos
</NavLink>

Aunque generalmente es mejor usar className por separación de responsabilidades.

Los estilos activos no son solo una cuestión estética, también mejoran significativamente la accesibilidad:

Beneficios para usuarios con lectores de pantalla

  • El atributo aria-current="page" indica claramente qué enlace representa la página actual
  • Los usuarios de lectores de pantalla reciben feedback audible sobre su ubicación
  • Mejora la navegación por teclado al poder identificar la página actual

Beneficios para usuarios con visión reducida

  • El contraste visual claro ayuda a orientarse
  • Los estilos distintivos (subrayado, color, peso) son más fáciles de identificar
  • Reduce la carga cognitiva al navegar

Buenas prácticas de accesibilidad

<NavLink
  to="/jobs"
  className={({ isActive }) => (isActive ? 'header-nav-link navLinkActive' : 'header-nav-link')}
  aria-label="Ir a empleos"
>
  Empleos
</NavLink>

React Router añade automáticamente aria-current="page" cuando está activo, así que no necesitas añadirlo manualmente.

Resumen

En esta clase hemos aprendido:

  • NavLink es como Link, pero sabe si su ruta está activa
  • La prop className puede ser una función que recibe isActive
  • Con isActive podemos devolver clases distintas y aplicar estilos activos
  • React Router añade aria-current="page" automáticamente, mejorando la accesibilidad
  • Con CSS podemos:
    • Cambiar color, peso, subrayado
    • Desactivar clic con pointer-events: none
    • Indicar visualmente que es la página actual
  • Podemos aplicar el patrón de abstracción también con NavLink
  • Los estilos activos mejoran tanto la UX como la accesibilidad

Con esto conseguimos: navegación más clara, accesible y con mejor experiencia para el usuario.