Tutorial WordPress: Estilizar anclas internas con offset y scroll-behavior smooth

·

·

Introducción

Este artículo describe, paso a paso y con todo detalle, cómo estilizar y conseguir un desplazamiento suave (smooth) en anclas internas en WordPress, teniendo en cuenta headers fijos o elementos que tapan la sección objetivo (offset). Incluye varias técnicas (puramente CSS, pseudo-elemento, y JavaScript dinámico), ejemplos listos para poner en tu tema o plugin, y consideraciones de accesibilidad y compatibilidad.

El problema

Cuando tienes un header fijo (sticky / fixed) en tu tema, los enlaces del tipo #seccion suelen desplazar la página hasta el elemento objetivo, pero el header lo cubre. Además, queremos que el desplazamiento sea suave. Hay varias formas de solucionarlo elegir la correcta depende de tu tema (header con altura fija o variable, WordPress admin bar presente, responsive, etc.).

Técnicas disponibles

Recomendación general

Si tu header tiene una altura conocida y estable, la solución CSS con scroll-margin-top es la más simple y robusta. Si el header cambia de tamaño (por ejemplo, cambia su altura en mobile, o hay elementos dinámicos) o si necesitas tratar correctamente la barra de administración de WordPress para usuarios logueados, la solución JavaScript que calcula la altura en tiempo real es la más flexible.

Implementación paso a paso

1) Método CSS moderno (recomendado cuando el header tiene altura fija)

Ventajas: simple, sin JS, funciona con los enlaces del navegador y con scrollIntoView nativo. Requisito: navegadores relativamente modernos (la gran mayoría actuales sí lo soportan).

:root {
  / Ajusta este valor a la altura real de tu header /
  --header-height: 72px
}

/ Smooth scrolling global /
html {
  scroll-behavior: smooth
}

/ Aplica offset a los elementos que pueden ser objetivo de ancla.
   Se suele aplicar a encabezados y elementos con id. /
h2[id],
h3[id],
h4[id],
section[id],
div[id] {
  scroll-margin-top: calc(var(--header-height)   1rem)
}

/ Si tu tema añade la barra admin de WP (users logueados),
   puedes ajustar la variable en CSS (si conoces la diferencia) /
body.admin-bar {
  --header-height: calc(72px   32px) / ejemplo: header 72   adminbar 32 /
}

Dónde ponerlo: Personalizador > CSS adicional, o style.css del tema hijo.

2) Método con pseudo-elemento (compatible incluso si scroll-margin-top no funciona)

Utiliza un pseudo-elemento invisible antes del objetivo para crear el espacio. Es una técnica clásica muy compatible.

:root {
  --header-height: 72px
}

/ Aplica a cualquier elemento con id (u opta por los selectores que uses) /
[id]::before {
  content: 
  display: block
  height: var(--header-height)
  margin-top: calc(-1  var(--header-height))
  visibility: hidden
  pointer-events: none
}

/ Smooth scrolling opcional /
html {
  scroll-behavior: smooth
}

/ Ajuste para admin-bar (ejemplo) /
body.admin-bar {
  --header-height: calc(72px   32px)
}

Nota: si aplicas esto sobre todos los [id], ten cuidado con elementos que dependen de flujo interno. Puedes usar una clase específica, por ejemplo .anchor-offset, y añadirla solo a los elementos que realmente quieres anclar.

3) Método JavaScript dinámico (mejor si el header cambia de tamaño)

Este script intercepta clicks en enlaces internos, calcula la altura real del header en ese momento (teniendo en cuenta admin bar y responsive), y hace scroll con behavior:smooth. También maneja carga directa con hash y mejora la accesibilidad poniendo foco en el objetivo.

/ Smooth scroll con offset dinámico para WordPress /
(function () {
  use strict

  // Selector que identifica tu header fijo
  var headerSelector = .site-header // ajusta según tu tema

  function getHeaderHeight() {
    var header = document.querySelector(headerSelector)
    var height = 0
    if (header) {
      height = header.getBoundingClientRect().height
    }
    // Si WP admin-bar está presente y es fija, sumamos su altura
    if (document.body.classList.contains(admin-bar)) {
      // valor aproximado la barra WP admin suele ser 32px (desktop)
      height  = 32
    }
    return Math.round(height)
  }

  function scrollToHash(hash, smooth) {
    if (!hash) return
    var id = hash.replace(#, )
    var target = document.getElementById(id)
    if (!target) return

    var headerHeight = getHeaderHeight()
    var rect = target.getBoundingClientRect()
    var absoluteY = window.pageYOffset   rect.top
    var offsetY = Math.max(0, absoluteY - headerHeight)

    if (smooth === false) {
      window.scrollTo(0, offsetY)
    } else {
      window.scrollTo({ top: offsetY, behavior: smooth })
    }

    // Accesibilidad: aseguramos foco en el elemento
    target.setAttribute(tabindex, -1)
    target.focus({ preventScroll: true })
    // opcional: limpiar tabindex después
    window.setTimeout(function () {
      target.removeAttribute(tabindex)
    }, 1000)
  }

  // Intercepta clicks en enlaces internos
  document.addEventListener(click, function (e) {
    var link = e.target
    // sube en el DOM si hace falta
    while (link  link.tagName !== A) {
      link = link.parentElement
    }
    if (!link) return

    var href = link.getAttribute(href)
    if (!href  href.indexOf(#) === -1) return

    // enlaces que solo cambian hash en misma página
    var origin = window.location.origin   window.location.pathname
    var a = document.createElement(a)
    a.href = href
    var samePage = (a.pathname === window.location.pathname)  (a.hostname === window.location.hostname)

    if (samePage  a.hash) {
      e.preventDefault()
      scrollToHash(a.hash, true)
      // Actualizar URL sin provocar salto extra
      history.pushState(null, , a.hash)
    }
  }, false)

  // Al cargar la página, si hay hash en la URL, desplazamos bien
  window.addEventListener(load, function () {
    if (location.hash) {
      // pequeño timeout para esperar estilos/layout
      setTimeout(function () {
        scrollToHash(location.hash, false)
      }, 10)
    }
  })

  // Si el usuario navega con back/forward y cambia el hash
  window.addEventListener(hashchange, function () {
    scrollToHash(location.hash, false)
  })
})()

4) Cómo añadir el script y CSS en WordPress

La forma correcta es encolar el script desde functions.php de tu tema hijo. A continuación un ejemplo que asume que crearás un archivo js personalizado en tu tema (assets/js/anchor-scroll.js).

/ functions.php del tema hijo /
function mi_tema_enqueue_anchor_scroll() {
  // Encola tu script (colócalo en /wp-content/themes/tu-tema-child/assets/js/anchor-scroll.js)
  wp_enqueue_script(
    mi-anchor-scroll,
    get_stylesheet_directory_uri() . /assets/js/anchor-scroll.js,
    array(), // dependencias si las hubiera
    1.0,
    true // enqueue in footer
  )

  // Si quieres pasar el selector del header desde PHP:
  config = array(
    headerSelector => .site-header // ajústalo aquí si prefieres
  )
  wp_localize_script( mi-anchor-scroll, MiAnchorScrollConfig, config )
}
add_action( wp_enqueue_scripts, mi_tema_enqueue_anchor_scroll )

En el archivo JS puedes leer MiAnchorScrollConfig.headerSelector para usar el selector provisto por PHP (si lo deseas).

Ejemplo completo mínimo (CSS JS)

/ CSS mínimo /
:root { --header-height: 72px }
html { scroll-behavior: smooth }
h2[id], h3[id], section[id] { scroll-margin-top: calc(var(--header-height)   1rem) }
/ JS mínimo: medir header y desplazar /
(function () {
  var headerSel = document.body.classList.contains(wp-theme) ? .site-header : .site-header
  function H() {
    var h = document.querySelector(headerSel)
    return h ? h.getBoundingClientRect().height : 0
  }
  document.addEventListener(click, function (e) {
    var a = e.target.closest  e.target.closest(a)
    if (!a) return
    if (!a.hash  a.origin !== location.origin) return
    if (a.pathname !== location.pathname) return
    e.preventDefault()
    var target = document.getElementById(a.hash.slice(1))
    if (!target) return
    var top = window.pageYOffset   target.getBoundingClientRect().top - H()
    window.scrollTo({ top: top, behavior: smooth })
    target.setAttribute(tabindex, -1) target.focus({ preventScroll: true })
    setTimeout(function(){ target.removeAttribute(tabindex) }, 1000)
    history.pushState(null, , a.hash)
  })
})()

Consideraciones de accesibilidad y compatibilidad

Notas finales prácticas

Recursos útiles



Leave a Reply

Your email address will not be published. Required fields are marked *