Por qué ocurre el problema y solución recomendada
Cuando en WordPress (o en cualquier web) tienes un header fijo (position: fixed o position: sticky) y usas enlaces internos (anclas con href=#mi-seccion), el navegador desplaza la ventana de modo que el elemento objetivo quede en la parte superior del viewport. Si el header ocupa parte de la pantalla, cubrirá la sección anclada. La solución moderna y sencilla es usar la propiedad CSS scroll-margin-top aplicada al elemento objetivo para desplazar su punto de anclaje hacia abajo la cantidad del header. Es la técnica preferida por ser simple, performant y compatible con los navegadores modernos.
Cómo aplicarlo: la versión CSS básica
Identifica qué elementos reciben enlaces internos en tu sitio: normalmente son los títulos de entradas y secciones (h2, h3, h4) o bloques del editor. Aplica scroll-margin-top igual a la altura del header fijo.
/ Ejemplo básico: header fijo de 80px /
h2[id], h3[id], h4[id] {
scroll-margin-top: 80px
}
/ Si usas bloques de Gutenberg con clase .wp-block-heading /
.wp-block-heading[id] {
scroll-margin-top: 80px
}
Recomendación: usar una variable CSS para mantener fácil el ajuste
Define una variable con la altura del header para poder cambiarla en un solo sitio cuando el header cambie en móvil o escritorio.
:root {
--site-header-height: 80px
}
/ Aplica la variable /
h2[id], h3[id], h4[id], .wp-block-heading[id] {
scroll-margin-top: var(--site-header-height)
}
Casos con header de altura dinámica (ej. responsive, menús colapsables)
Si el header cambia de altura según el dispositivo o cuando el usuario abre/cierra el menú, la variable CSS fija puede no ser suficiente. Tres enfoques prácticos:
- Variables CSS cambiadas con media queries: actualizar –site-header-height por breakpoint.
- Calcular el offset con JavaScript y aplicar inline a los objetivos (mejor para alturas dinámicas).
- Combinar ambas: variable CSS para la mayoría de casos y JS para ajustes en tiempo real (resize, cambios de clase).
Ejemplo JavaScript: calcular y fijar scroll-margin-top dinámicamente
Este script detecta la altura real del header y aplica scroll-margin-top a elementos con id usados como objetivos de anclaje. Útil como fallback o para headers con altura variable.
/ Ajuste dinámico de scroll-margin-top según altura del header /
(function(){
function ajustarOffsets() {
var header = document.querySelector(.site-header) document.querySelector(header)
if (!header) return
var offset = header.getBoundingClientRect().height
var targets = document.querySelectorAll(h2[id], h3[id], h4[id], .wp-block-heading[id], [id].anchor-target)
targets.forEach(function(el){
el.style.scrollMarginTop = (offset 8) px // 8px para un pequeño margen visual
})
}
// Ejecutar al cargar y cuando cambie tamaño
window.addEventListener(load, ajustarOffsets)
window.addEventListener(resize, function(){
// Debounce simple
clearTimeout(window._ajustarOffsetsTimeout)
window._ajustarOffsetsTimeout = setTimeout(ajustarOffsets, 120)
})
// Opcional: observar cambios en el DOM si el header cambia de tamaño por clases
var header = document.querySelector(.site-header) document.querySelector(header)
if (header window.MutationObserver) {
var observer = new MutationObserver(function(){ ajustarOffsets() })
observer.observe(header, { attributes: true, attributeFilter: [class, style] })
}
})()
Alternativa: interceptar clicks en enlaces internos y desplazar con offset
En lugar de modificar los elementos objetivo, puedes interceptar los clicks en enlaces internos y desplazar manualmente restando el offset del header. Útil cuando no puedes modificar los objetivos o quieres animación personalizada.
/ Intercepta clicks en enlaces internos y aplica offset /
(function(){
function getHeaderOffset(){
var header = document.querySelector(.site-header) document.querySelector(header)
return header ? header.getBoundingClientRect().height : 0
}
document.addEventListener(click, function(e){
var a = e.target.closest(a[href^=#])
if (!a) return
var hash = a.getAttribute(href)
if (hash === # hash === ) return
var target = document.getElementById(hash.slice(1))
if (!target) return
// Evitar comportamiento por defecto y hacer scroll con offset
e.preventDefault()
var rect = target.getBoundingClientRect()
var scrollTop = window.pageYOffset document.documentElement.scrollTop
var top = rect.top scrollTop - getHeaderOffset() - 8 // 8px margen
window.scrollTo({ top: top, behavior: smooth })
// Actualizar hash sin salto inmediato (opcional)
history.pushState(null, , hash)
// Fijar foco al target para accesibilidad
target.setAttribute(tabindex, -1)
target.focus({ preventScroll: true })
})
})()
Integración en WordPress: dónde añadir CSS y JS
- CSS: Appearance → Customize → Additional CSS, o en el archivo style.css de un child theme (si modificas un tema padre usa child theme).
- JS: Si solo son unas pocas líneas, puedes insertarlo con un plugin de snippets o mediante functions.php del child theme añadiendo un script encolado en el footer.
Ejemplo PHP: encolar un script en functions.php
/ functions.php - encolar script para ajustar scroll offsets /
function tema_personalizado_enqueue_scripts() {
wp_enqueue_script(
ajuste-scroll-offset,
get_stylesheet_directory_uri() . /js/ajuste-scroll-offset.js,
array(),
1.0,
true
)
}
add_action(wp_enqueue_scripts, tema_personalizado_enqueue_scripts)
Compatibilidad y buenas prácticas
- Compatibilidad: scroll-margin-top funciona en la mayoría de navegadores modernos (Chromium, Firefox, Safari actuales). Para navegadores antiguos usa el fallback JS.
- Accesibilidad: cuando manipules el foco tras el scroll, asegúrate de que el elemento sea focusable (tabindex=-1) y que el cambio de foco no confunda al lector de pantalla.
- Evita hacks como padding negativo o margenes extra en el documento: pueden romper layout y comportamiento de lectura por pantalla.
- Documenta la variable de altura del header: tiene sentido definirse en :root para que otros desarrolladores la usen y actualicen con cambios de diseño.
Trucos adicionales y casos especiales
- Si solo quieres que el offset afecte a anclajes cuando se navega desde otra página: puedes comprobar si page load contiene location.hash y aplicar scroll en load con offset.
- Cuando uses CSS frameworks o plugins que añaden anchors automáticos: inspecciona qué selector usan y aplica scroll-margin-top a esos selectores (p. ej. .anchor, .wp-block-heading[id], .toc-anchor, etc.).
- Transiciones y smooth scroll: combina offset con CSS scroll-behavior: smooth en html { scroll-behavior: smooth } o usa la opción behavior:smooth en JS. Ten en cuenta que scroll-margin-top y scroll-behavior funcionan bien juntos.
- Usar :target con padding-top negativo: técnica antigua que consiste en usar :target pseudo-elemento para crear espacio, pero es menos limpia que scroll-margin-top y puede romper el flujo en móviles.
Tabla comparativa rápida
| Método | Ventajas | Inconvenientes |
|---|---|---|
| scroll-margin-top | Simple, CSS-only, performante | Requiere conocer la altura del header no funciona en navegadores muy antiguos |
| JS que aplica inline scrollMarginTop | Dinámico, adapta a cambios de altura | Dependiente de JavaScript |
| Interceptar clicks y calcular offset | Total control sobre animación y offset | Más código, debe mantener el focus/estado del hash |
Ejemplos prácticos listos para usar
CSS recomendada (variable selectores comunes):
:root {
--site-header-height: 72px / Ajusta según tu header /
}
/ Aplica a títulos y bloques con id (Gutenberg) /
h1[id], h2[id], h3[id], h4[id],
.wp-block-heading[id],
[id].anchor-target {
scroll-margin-top: var(--site-header-height)
}
/ Ajuste opcional para móviles usando media queries /
@media (max-width: 768px) {
:root { --site-header-height: 56px }
}
PHP para encolar el script (añádelo en functions.php del child theme):
function enqueue_custom_scroll_offset_script(){
wp_enqueue_script(
custom-scroll-offset,
get_stylesheet_directory_uri() . /js/custom-scroll-offset.js,
array(),
1.0,
true
)
}
add_action(wp_enqueue_scripts, enqueue_custom_scroll_offset_script)
JS que calcula el offset y aplica a los objetivos (guardar como js/custom-scroll-offset.js):
(function(){
function setOffsets() {
var header = document.querySelector(.site-header) document.querySelector(header)
if (!header) return
var offset = header.getBoundingClientRect().height
document.querySelectorAll(h1[id], h2[id], h3[id], h4[id], .wp-block-heading[id]).forEach(function(el){
el.style.scrollMarginTop = (offset 6) px
})
}
window.addEventListener(load, setOffsets)
window.addEventListener(resize, function(){
clearTimeout(window._offsetTimer)
window._offsetTimer = setTimeout(setOffsets, 120)
})
})()
Resumen final
La forma más limpia y recomendable es usar scroll-margin-top aplicado a los elementos que son objetivos de anclas. Para headers de altura variable o situaciones complejas añade un pequeño script que calcule la altura real y establezca el offset dinámicamente. Integra el CSS en el customizer o en el child theme y encola el JavaScript desde functions.php. No olvides comprobar accesibilidad (gestión del foco) y probar en móvil y escritorio.
Leave a Reply