jQuery / Fijar un menú arriba al desplazarse

Compare el desplazamiento del documento con la posición original del menú y active una clase position:fixed al alcanzarla. Quítela al volver arriba. Conserve un espacio de igual altura para que sacar el menú del flujo no haga saltar el contenido.

Límite estable y espacio reservado

CSS position:fixed sale del flujo y normalmente se sitúa respecto a la ventana. top:0 es el borde superior. La posición original pertenece al documento; use coordenadas compatibles.

No mida el menú ya fijado como su posición original. #menu-slot permanece en el documento. Su top visible más window.scrollY da el umbral documental.

El menú fijo puede dejar de seguir el ancho del padre. Asigne left y width desde el espacio y conserve su altura con borde. Sin margen y con border-box, coinciden los límites.

Ejemplo completo de desplazamiento y tamaño

Guarde todo en UTF-8 como fixed-menu-demo.html y ábralo en el navegador. Internet carga el archivo oficial de jQuery 4.0.0. No requiere servidor ni paquetes. Umbral, posición del contenido, cambios de ancho y anclas se comprobaron en Edge para Windows con 3.7.1 y 4.0.0.

La cabecera mide 180px, por lo que el umbral inicial es 180. Controles y estado inferiores son herramientas de práctica que debe adaptar o retirar.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>A menu fixed after its original position</title>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
  <style>
    :root { --menu-height: 60px; }
    html { scroll-behavior: auto; }
    body { margin: 0; font: 16px/1.5 sans-serif; }
    header { height: 180px; display: grid; place-items: center; background: #e7f2f2; }
    .wrap { width: 100%; max-width: 800px; padding: 0 20px; margin: 0 auto; box-sizing: border-box; }
    #menu { box-sizing: border-box; width: 100%; display: flex; flex-wrap: wrap; gap: 12px; padding: 12px; border: 2px solid teal; background: #fff; }
    #menu a { padding: 4px; color: #075c55; }
    #menu.is-fixed { position: fixed; top: 0; z-index: 20; box-shadow: 0 3px 8px #0003; }
    main section { min-height: 650px; padding-top: 24px; }
    main h2 { margin: 0; scroll-margin-top: calc(var(--menu-height) + 16px); }
    #controls { position: fixed; right: 8px; bottom: 8px; z-index: 30; background: #fff; padding: 4px; }
    button { font: inherit; margin: 2px; }
    #result { position: fixed; left: 8px; bottom: 70px; background: #fff; white-space: pre-wrap; padding: 6px; z-index: 30; }
  </style>
</head>
<body>
  <header>Header before the menu</header>
  <div class="wrap"><div id="menu-slot">
    <nav id="menu" aria-label="Sections"><a href="#alpha">Alpha section</a><a href="#beta">Beta section</a><a href="#gamma">Gamma section</a></nav>
  </div></div>
  <main class="wrap">
    <section><h2 id="alpha">Alpha</h2><p>First section content.</p></section>
    <section><h2 id="beta">Beta</h2><p>Second section content.</p></section>
    <section><h2 id="gamma">Gamma</h2><p>Third section content.</p></section>
  </main>
  <div id="controls"><button type="button" id="down">Go to 300px</button><button type="button" id="reset">Back to 0px</button></div>
  <pre id="result" aria-live="polite"></pre>
  <script>
    $(function () {
      const $menu = $("#menu"), $slot = $("#menu-slot");
      let scheduled = false;
      function update() {
        scheduled = false;
        const rect = $slot[0].getBoundingClientRect();
        const threshold = rect.top + window.scrollY;
        const fixed = $(window).scrollTop() >= threshold;
        $menu.toggleClass("is-fixed", fixed).css({
          left: fixed ? rect.left + "px" : "",
          width: fixed ? rect.width + "px" : ""
        });
        const height = $menu.outerHeight();
        $slot.css("height", height + "px");
        document.documentElement.style.setProperty("--menu-height", height + "px");
        $("#result").text(`menu=${fixed ? "fixed" : "flow"}\nthreshold=${Math.round(threshold)}\nscrollY=${Math.round(window.scrollY)}`);
      }
      function schedule() {
        if (!scheduled) { scheduled = true; window.requestAnimationFrame(update); }
      }
      $(window).on("scroll.fixedMenuLesson resize.fixedMenuLesson load.fixedMenuLesson", schedule);
      $("#down").on("click", function () { window.scrollTo(0, 300); schedule(); });
      $("#reset").on("click", function () { window.scrollTo(0, 0); schedule(); });
      update();
    });
  </script>
</body>
</html>

Salida inicial arriba. flow significa colocación normal.

menu=flow
threshold=180
scrollY=0
  1. Desplace despacio: flujo normal en 179px y fijación en top=0 desde 180px.
  2. Go to 300px da menu=fixed, threshold=180 y scrollY=300. El contenido no debe saltar una altura de menú.
  3. Amplíe y reduzca: izquierda y ancho siguen el contenedor; los saltos de línea ajustan la altura reservada.
  4. Beta section sigue el ancla real #beta con el título visible debajo. Los enlaces mantienen foco y uso normal del teclado.
  5. Back to 0px restaura flujo y limpia left y width fijos. No elimina el fragmento URL existente.

Actualizaciones y separación de anclas

Compare $(window).scrollTop() con el umbral. .toggleClass("is-fixed", fixed) establece el booleano explícito. Un toggle simple en cada scroll alternaría clases por número de eventos.

scroll, resize y load programan update() con requestAnimationFrame() antes del siguiente repintado. scheduled evita callbacks pendientes duplicados. No garantiza milisegundos ni una tasa de cuadros; mantenga ligeras las lecturas y escrituras.

Se establece ancho antes de medir altura para considerar enlaces que se parten. update() inicial trata posiciones restauradas. Si después crecen contenido superior o fuentes, conecte schedule() al finalizar. No se observa automáticamente todo cambio del DOM.

--menu-height más 16px define scroll-margin-top de los títulos. Las anclas nativas no quedan tapadas. Otra cabecera fija exige top y separación adaptados. Conserve href reales y no impida navegación sin necesidad.

CSS sticky y comprobaciones reales

Para adherirse simplemente arriba, position:sticky y top pueden bastar sin JavaScript. Conserva espacio y depende del contenedor desplazable y bloque contenedor. Revise padres cortos y antecesores overflow; no equivale a fixed.

El ejemplo mueve el documento completo, no un panel interno. Antecesores transformados pueden cambiar la referencia de fixed. Examine estructura sin sobrescribir reglas globales position u overflow.

Un z-index alto no soluciona todo modal o contexto de apilamiento. Compruebe contenido móvil, foco visible y enlaces al ampliar. Quite o recoloque superposiciones de práctica.

Al desmontar, restaure clase fija, left/width, altura y propiedad personalizada dentro del ámbito de la función, además de eventos. Inicializar varias veces exige gestionar el ciclo de vida para evitar duplicados.