<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Giulio</title>
    <description>The latest articles on DEV Community by Giulio (@giulio_2708401a53dddb7071).</description>
    <link>https://dev.to/giulio_2708401a53dddb7071</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3966684%2F5a46af25-6b15-4a08-9b22-4f04fd957b87.png</url>
      <title>DEV Community: Giulio</title>
      <link>https://dev.to/giulio_2708401a53dddb7071</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/giulio_2708401a53dddb7071"/>
    <language>en</language>
    <item>
      <title>⭐ One line of code that fixed my jumping sticky header</title>
      <dc:creator>Giulio</dc:creator>
      <pubDate>Wed, 03 Jun 2026 14:00:27 +0000</pubDate>
      <link>https://dev.to/giulio_2708401a53dddb7071/one-line-of-code-that-fixed-my-jumping-sticky-header-gik</link>
      <guid>https://dev.to/giulio_2708401a53dddb7071/one-line-of-code-that-fixed-my-jumping-sticky-header-gik</guid>
      <description>&lt;p&gt;You've built a sticky header. It works. You ship it.&lt;/p&gt;

&lt;p&gt;Then someone opens your site on a slow connection, scrolls down, and the whole page jolts — content jumps up by a few pixels right as the header sticks. It looks broken. And the worst part? You can't reproduce it locally, because on your machine the fonts are already cached.&lt;/p&gt;

&lt;p&gt;I hit this exact bug in a navigation plugin I maintain. Here's what's actually happening, and the fix that finally killed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;A sticky header that's &lt;code&gt;position: fixed&lt;/code&gt; is pulled out of the document flow. To stop the content below from jumping up the moment the header detaches, you insert a placeholder — a spacer — with the same height as the header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.site-header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;spacer&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.header-spacer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;spacer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetHeight&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Measure the header once, set the spacer, done. On every demo this works perfectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it breaks in the real world
&lt;/h2&gt;

&lt;p&gt;The trap is &lt;em&gt;when&lt;/em&gt; you measure.&lt;/p&gt;

&lt;p&gt;If your header uses a custom font (Google Fonts, a self-hosted webfont, anything loaded asynchronously), the browser renders the page first with a fallback system font, then swaps in the real font once it downloads. This is the &lt;code&gt;font-display: swap&lt;/code&gt; behavior — great for performance, brutal for layout measurements.&lt;/p&gt;

&lt;p&gt;So the sequence on a cold load is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Page renders with &lt;strong&gt;Arial&lt;/strong&gt; (fallback). Your header is, say, 64px tall.&lt;/li&gt;
&lt;li&gt;Your JS runs, measures 64px, sets the spacer to 64px. &lt;/li&gt;
&lt;li&gt;The webfont finishes downloading. Your header re-renders in &lt;strong&gt;Poppins&lt;/strong&gt;, which has a slightly taller line-height. Now the header is 68px tall.&lt;/li&gt;
&lt;li&gt;The spacer is still &lt;strong&gt;64px&lt;/strong&gt;. There's now a 4px mismatch — and the moment the header goes sticky, the content jumps to close that gap.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On your dev machine steps 3 happens instantly (font is cached), so you never see it. On a real visitor's first load, the font arrives hundreds of milliseconds later. That's the jump.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The browser gives you a promise that resolves when all fonts have finished loading: &lt;code&gt;document.fonts.ready&lt;/code&gt;. Re-measure when it fires.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;recalcSpacer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;spacer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetHeight&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Measure now (fallback font)...&lt;/span&gt;
&lt;span class="nf"&gt;recalcSpacer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// ...and again once the real fonts are in.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fonts&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fonts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fonts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recalcSpacer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Fonts aside, the header also changes height on resize.&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recalcSpacer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the core of it. But there was one more wrinkle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wrinkle: measuring an element that's already fixed
&lt;/h2&gt;

&lt;p&gt;If the header is &lt;em&gt;already&lt;/em&gt; sticky when the font swaps (visitor scrolled fast), &lt;code&gt;offsetHeight&lt;/code&gt; can return the wrong value, because a fixed element's box doesn't always reflect what the spacer needs. The fix is to measure it in its natural state: briefly remove the fixed class, read the height, put it back — all synchronously, so the browser never paints the intermediate state and the user sees nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;recalcSpacerForce&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;wasFixed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;is-sticky&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wasFixed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;is-sticky&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;spacer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetHeight&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wasFixed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;is-sticky&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fonts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recalcSpacerForce&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the class is removed and re-added within the same synchronous block — no &lt;code&gt;await&lt;/code&gt;, no &lt;code&gt;setTimeout&lt;/code&gt; — the layout change never reaches the screen. The browser batches it. You get the correct measurement with zero visible flicker.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Any layout measurement you take before fonts load is provisional. If you're setting heights, offsets, or scroll thresholds based on text-containing elements, hook into &lt;code&gt;document.fonts.ready&lt;/code&gt; and measure again. It's one line, and it removes a class of "it works on my machine" bugs that are almost impossible to catch in dev.&lt;/p&gt;

&lt;p&gt;I shipped this in a free WordPress menu plugin (Giuliomax Menu Builder) where the sticky header is user-configurable with any Google Font — which is exactly the scenario that surfaces this bug at scale. If you want to see it in a real codebase: &lt;a href="https://github.com/Giulio001/menux-free-version" rel="noopener noreferrer"&gt;https://github.com/Giulio001/menux-free-version&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Has this one bitten you too? Curious how others handle layout measurements around async fonts.&lt;/p&gt;

&lt;p&gt;wordpress: &lt;a href="https://wordpress.org/plugins/giuliomax-menu-builder/" rel="noopener noreferrer"&gt;https://wordpress.org/plugins/giuliomax-menu-builder/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
