Guía detallada

Guía de Implementación de Skip Links: Patrones Completos para Navegación de Bypass

Ver contenido

Introducción

Los skip links son una de las características de accesibilidad más importantes para usuarios de teclado, sin embargo frecuentemente se implementan incorrectamente o se omiten por completo. Esta guía cubre todo desde implementación básica hasta patrones avanzados, incluyendo el manejo de aplicaciones de página única, múltiples destinos de salto, y soluciones específicas para frameworks.

Un skip link bien implementado ahorra a los usuarios de teclado navegar a través de potencialmente docenas de enlaces de navegación en cada carga de página. Para sitios con headers extensos, esto puede significar la diferencia entre una experiencia usable e inutilizable.

Estructura HTML Mínima

<!DOCTYPE html>
<html lang="es">
<head>
  <title>Título de Página</title>
</head>
<body>
  <!-- Skip link DEBE ser el primer elemento enfocable -->
  <a href="#main-content" class="skip-link">
    Saltar al contenido principal
  </a>

  <header>
    <nav>
      <!-- Enlaces de navegación -->
    </nav>
  </header>

  <main id="main-content" tabindex="-1">
    <h1>Encabezado de Página</h1>
    <!-- Contenido principal -->
  </main>

  <footer>
    <!-- Contenido del footer -->
  </footer>
</body>
</html>

CSS Esencial

/* Visualmente oculto pero accesible */
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: #000000;
  color: #ffffff;
  padding: 8px 16px;
  z-index: 100;
  text-decoration: none;
  font-weight: bold;
}

/* Visible al recibir foco */
.skip-link:focus {
  top: 0;
}

Por Qué tabindex=“-1” en el Destino

El elemento destino necesita tabindex="-1" para recibir foco programáticamente:

<!-- Sin tabindex: El foco puede no moverse en algunos navegadores -->
<main id="main-content">

<!-- Con tabindex: Foco garantizado en todos los navegadores -->
<main id="main-content" tabindex="-1">

Importante: tabindex="-1" hace el elemento enfocable vía JavaScript/enlaces ancla pero lo mantiene fuera del orden normal de tab.

Patrones CSS Avanzados

Animación Suave

.skip-link {
  position: absolute;
  transform: translateY(-100%);
  transition: transform 0.3s ease-in-out;
  background: #1a1a1a;
  color: #ffffff;
  padding: 12px 24px;
  border-radius: 0 0 4px 4px;
  z-index: 9999;
  text-decoration: none;
  font-size: 14px;
  font-weight: 600;
}

.skip-link:focus {
  transform: translateY(0);
}

/* Remover outline ya que tenemos foco visual claro */
.skip-link:focus {
  outline: 2px solid #ffffff;
  outline-offset: 2px;
}
.skip-link {
  position: absolute;
  left: 50%;
  transform: translateX(-50%) translateY(-100%);
  transition: transform 0.3s ease;
  /* ... otros estilos */
}

.skip-link:focus {
  transform: translateX(-50%) translateY(0);
}
.skip-link {
  position: absolute;
  top: -100%;
  left: 16px;
  background: #000000;
  color: #ffffff;
  padding: 16px 24px;
  border: 3px solid #ffffff;
  border-radius: 4px;
  font-size: 16px;
  font-weight: bold;
  text-decoration: underline;
  z-index: 10000;
  transition: top 0.2s;
}

.skip-link:focus {
  top: 16px;
}

/* Asegurar que funciona en Modo de Alto Contraste de Windows */
@media (forced-colors: active) {
  .skip-link {
    border: 3px solid currentColor;
  }
}

Cuándo Usar Múltiples Enlaces

Usa múltiples skip links cuando tu página tiene:

  • Navegación secundaria o búsqueda significativa
  • Múltiples áreas de contenido principal
  • Layouts de aplicación complejos

Patrón de Implementación

<div class="skip-links">
  <a href="#main-content" class="skip-link">Saltar al contenido principal</a>
  <a href="#search" class="skip-link">Saltar a búsqueda</a>
  <a href="#navigation" class="skip-link">Saltar a navegación</a>
</div>

<header>
  <form id="search" role="search" tabindex="-1">
    <input type="search" aria-label="Buscar">
    <button type="submit">Buscar</button>
  </form>
  <nav id="navigation" tabindex="-1">
    <!-- Navegación -->
  </nav>
</header>

<main id="main-content" tabindex="-1">
  <!-- Contenido -->
</main>

CSS para Múltiples Enlaces

.skip-links {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 10000;
}

.skip-link {
  position: absolute;
  top: -100%;
  left: 0;
  display: block;
  padding: 12px 24px;
  background: #000;
  color: #fff;
  text-decoration: none;
  white-space: nowrap;
  transition: top 0.2s;
}

/* Apilar enlaces verticalmente cuando tienen foco */
.skip-link:nth-child(1):focus { top: 0; }
.skip-link:nth-child(2):focus { top: 48px; }
.skip-link:nth-child(3):focus { top: 96px; }

Implementaciones en Frameworks

Componente React

// SkipLink.tsx
import { useRef, useEffect } from 'react';

interface SkipLinkProps {
  targetId: string;
  children: React.ReactNode;
}

export function SkipLink({ targetId, children }: SkipLinkProps) {
  const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
    e.preventDefault();
    const target = document.getElementById(targetId);

    if (target) {
      target.setAttribute('tabindex', '-1');
      target.focus();
      target.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }
  };

  return (
    <a
      href={`#${targetId}`}
      className="skip-link"
      onClick={handleClick}
    >
      {children}
    </a>
  );
}

// Uso en App.tsx
function App() {
  return (
    <>
      <SkipLink targetId="main-content">
        Saltar al contenido principal
      </SkipLink>
      <Header />
      <main id="main-content">
        {/* Contenido */}
      </main>
    </>
  );
}

Componente Vue 3

<!-- SkipLink.vue -->
<script setup lang="ts">
interface Props {
  targetId: string
}

const props = defineProps<Props>()

function handleClick(e: Event) {
  e.preventDefault()
  const target = document.getElementById(props.targetId)

  if (target) {
    target.setAttribute('tabindex', '-1')
    target.focus()
    target.scrollIntoView({ behavior: 'smooth', block: 'start' })
  }
}
</script>

<template>
  <a
    :href="`#${targetId}`"
    class="skip-link"
    @click="handleClick"
  >
    <slot>Saltar al contenido principal</slot>
  </a>
</template>

<style scoped>
.skip-link {
  position: absolute;
  transform: translateY(-100%);
  transition: transform 0.2s ease-in-out;
  background: #000;
  color: #fff;
  padding: 0.75rem 1.5rem;
  z-index: 9999;
  text-decoration: none;
}

.skip-link:focus {
  transform: translateY(0);
}
</style>

Consideraciones para Aplicaciones de Página Única

Manejo de Cambio de Ruta

En SPAs, necesitas gestionar el foco cuando las rutas cambian:

// Ejemplo con React Router
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function useRouteAnnouncer() {
  const location = useLocation();

  useEffect(() => {
    // Enfocar contenido principal en cambio de ruta
    const main = document.getElementById('main-content');
    if (main) {
      main.tabIndex = -1;
      main.focus();

      // Remover tabindex después del foco (opcional)
      main.addEventListener('blur', () => {
        main.removeAttribute('tabindex');
      }, { once: true });
    }
  }, [location.pathname]);
}

Integración con Vue Router

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [...],
  scrollBehavior(to, from, savedPosition) {
    // Enfocar contenido principal después de navegación
    setTimeout(() => {
      const main = document.getElementById('main-content')
      if (main) {
        main.setAttribute('tabindex', '-1')
        main.focus()
      }
    }, 100)

    return savedPosition || { top: 0 }
  }
})

Manejando Headers Sticky

El Problema

Cuando los destinos de skip link están cerca de la parte superior de la página, los headers sticky pueden cubrirlos:

/* Skip link funciona pero el contenido queda oculto bajo el header */
.header {
  position: sticky;
  top: 0;
  height: 80px;
}

#main-content {
  /* El contenido comienza justo bajo el header visualmente,
     pero el skip link hace scroll a la posición real del elemento */
}

Solución: Scroll Padding

html {
  scroll-padding-top: 100px; /* Altura del header + margen */
}

/* O apuntar a elementos específicos */
#main-content,
#search,
#footer {
  scroll-margin-top: 100px;
}

Solución: Propiedades Personalizadas CSS

:root {
  --header-height: 80px;
  --scroll-buffer: 20px;
}

html {
  scroll-padding-top: calc(var(--header-height) + var(--scroll-buffer));
}

/* Actualizar con JavaScript si cambia la altura del header */

Solución: Offset con JavaScript

function handleSkipLink(e) {
  e.preventDefault();
  const targetId = e.target.getAttribute('href').slice(1);
  const target = document.getElementById(targetId);
  const headerHeight = document.querySelector('.header').offsetHeight;

  if (target) {
    const targetPosition = target.getBoundingClientRect().top + window.scrollY;
    window.scrollTo({
      top: targetPosition - headerHeight - 20,
      behavior: 'smooth'
    });
    target.setAttribute('tabindex', '-1');
    target.focus({ preventScroll: true });
  }
}

Checklist de Pruebas Manuales

  1. Prueba de primer enfocable:

    • Cargar página fresca
    • Presionar Tab una vez
    • Skip link debe ser visible
  2. Prueba de activación:

    • Enfocar skip link
    • Presionar Enter
    • El foco debe moverse al destino
    • La página debe hacer scroll si es necesario
  3. Prueba post-salto:

    • Después de activar skip link
    • Presionar Tab
    • Debe enfocar el primer elemento interactivo en el contenido principal
  4. Prueba con lector de pantalla:

    • Navegar con lector de pantalla
    • Skip link debe ser anunciado
    • El destino debe ser anunciado después de activación

Pruebas Automatizadas

// Test con Playwright
import { test, expect } from '@playwright/test';

test.describe('Skip Links', () => {
  test('skip link es el primer elemento enfocable', async ({ page }) => {
    await page.goto('/');
    await page.keyboard.press('Tab');

    const focused = await page.locator(':focus');
    await expect(focused).toHaveClass(/skip-link/);
    await expect(focused).toBeVisible();
  });

  test('skip link mueve el foco al contenido principal', async ({ page }) => {
    await page.goto('/');
    await page.keyboard.press('Tab');
    await page.keyboard.press('Enter');

    const focused = await page.locator(':focus');
    await expect(focused).toHaveAttribute('id', 'main-content');
  });

  test('skip link es visible solo con foco', async ({ page }) => {
    await page.goto('/');

    const skipLink = page.locator('.skip-link');

    // No visible inicialmente
    await expect(skipLink).not.toBeInViewport();

    // Visible con foco
    await page.keyboard.press('Tab');
    await expect(skipLink).toBeInViewport();

    // Oculto de nuevo después de blur
    await page.keyboard.press('Tab');
    await expect(skipLink).not.toBeInViewport();
  });
});

Errores Comunes y Soluciones

<!-- INCORRECTO: El enlace del logo viene primero -->
<body>
  <a href="/" class="logo">Inicio</a>
  <a href="#main" class="skip-link">Saltar</a>
</body>

<!-- CORRECTO: Skip link es primero -->
<body>
  <a href="#main" class="skip-link">Saltar</a>
  <a href="/" class="logo">Inicio</a>
</body>

Error 2: El Destino No Existe

<!-- INCORRECTO: IDs no coinciden -->
<a href="#main-content">Saltar</a>
<main id="content">...</main>

<!-- CORRECTO: IDs coinciden -->
<a href="#main-content">Saltar</a>
<main id="main-content">...</main>

Error 3: Usar display:none

/* INCORRECTO: Remueve del árbol de accesibilidad */
.skip-link {
  display: none;
}
.skip-link:focus {
  display: block;
}

/* CORRECTO: Visualmente oculto pero accesible */
.skip-link {
  position: absolute;
  top: -9999px;
}
.skip-link:focus {
  top: 0;
}

Checklist de Resumen

  • [ ] Skip link es el primer elemento enfocable en el DOM
  • [ ] Skip link está visualmente oculto hasta recibir foco
  • [ ] Skip link usa texto claro y descriptivo
  • [ ] El elemento destino tiene ID coincidente
  • [ ] El destino tiene tabindex=“-1” para foco confiable
  • [ ] La página hace scroll para mostrar el destino cuando se activa
  • [ ] Headers sticky no oscurecen el destino
  • [ ] Funciona correctamente después de cambios de ruta en SPA
  • [ ] Probado con teclado y lector de pantalla
  • [ ] Compatible con modo de alto contraste

Artículos Relacionados


Referencias

  1. W3C - WCAG 2.2 SC 2.4.1 Bypass Blocks
  2. W3C - Technique G1: Skip Link
  3. W3C - Technique G124: Adding Links at Top
  4. WebAIM - Skip Navigation Links
  5. A11Y Project - Skip Navigation Links

Artículos relacionados

Versión relacionada

Introducción

Skip Links: Proporcionando Atajos al Contenido Principal

Los skip links son atajos de navegación ocultos que permiten a los usuarios de teclado saltar contenido repetitivo—como headers y menús de navegación—e ir...

Hub de categoría

Hub

Hub de Cumplimiento WCAG 2.2: Guía Completa de Accesibilidad Web

La accesibilidad web asegura que los sitios web y aplicaciones puedan ser utilizados por todos, incluyendo personas con discapacidades