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
- CSS moderno: usar scroll-margin-top scroll-behavior: smooth.
- Pseudo-elemento: crear un espacio invisible arriba del objetivo con ::before y negativo margin para que el ancla quede visible.
- JavaScript: interceptar clicks y/o el hash al cargar, calcular la altura real del header y hacer scroll con behavior:smooth.
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
- Preferencias de movimiento: respeta prefers-reduced-motion. Si el usuario la solicita, evita animaciones suaves. Puedes detectar con CSS o JS y usar scroll-behavior: auto en ese caso.
- Focus: tras el scroll el foco debe estar en el objetivo para que usuarios de teclado y lectores de pantalla sepan dónde están. El ejemplo JS pone tabindex=-1 temporalmente y hace focus.
- Admin bar: WordPress añade body.admin-bar para usuarios logueados ten esto en cuenta sumando su altura si es necesario.
- Enlaces desde otras páginas: si el usuario llega con un hash en la URL (p. ej. /pagina/#seccion), el script maneja el scroll en load/hashchange si usas solo CSS (scroll-margin-top) esto también funcionará automáticamente.
- Soporte de navegadores: scroll-behavior y scroll-margin-top están soportados por navegadores modernos. Para navegadores antiguos, usa la solución pseudoelemento o el fallback JS.
Notas finales prácticas
- Si tu header tiene tamaño diferente en mobile, establece la variable CSS (–header-height) mediante media queries o usa la versión JS para calcular en cada momento.
- Para cambios inmediatos en el tema (ej. añadir una clase al header cuando se hace scroll), la solución JS que recalcula es la más robusta.
- Prueba siempre con usuarios logueados y deslogueados (admin-bar) y en tamaños de pantalla diferentes.
Leave a Reply