67

I'm trying to make a web page with a fixed header and a scrollable content area. This is trivial when the header has a known height but I'm struggling to find a solution for when the header is fluid.

The layout I want is:

--------------
head
--------------
content
--------------

where "head" is whatever height its content needs it to be and "content" has no minimum height but will reach a maximum height of the bottom of the viewport before becoming scrollable.

Is this possible these days in pure CSS? I'm targeting IE8+.

To clarify what I want, here is what I would do if I knew the height of the header:

<!DOCTYPE html>
<html>
    <head>
        <style type="text/css">

body {
    margin: 0;
}

#head {
    background: yellow;
    height: 20px; /* I can't rely on knowing this. */
}

#content {
    background: red;
    position: absolute;
    top: 20px; /* here also */
    bottom: 0;
    width: 100%;
    overflow: auto;
}

        </style>
    </head>
    <body>
        <div id="head">some variable height content</div>
        <div id="content">
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
        </div>
    </body>
</html>
coffee-grinder
  • 26,940
  • 19
  • 56
  • 82
David Roe
  • 1,228
  • 1
  • 11
  • 15

8 Answers8

31

Assuming by "fixed" you mean position:fixed, I don't think it's possible in pure CSS, as position:fixed takes the element out of the document flow.

However, it should just take a line or two of JavaScript to get what you want. Something like this (untested, only for example purposes, will need syntax tweaked to actually work):

var height = document.getElementById("head").offsetHeight;
document.getElementById("content").style.marginTop = height + 'px';

Something like that should get you the rendered height of the fixed <div> and set the content <div>'s margin accordingly. You'll also need to explicitly set a background color on the fixed <div>, otherwise the content will appear to bleed into the fixed one when scrolling.

Useless Code
  • 12,123
  • 5
  • 35
  • 40
Shauna
  • 9,495
  • 2
  • 37
  • 54
  • 9
    Don't forget to recalculate after the page is resized – Eric Jun 20 '11 at 16:58
  • I can design the header so it won't re-flow when its width changes so I shouldn't have to worry about resizing. – David Roe Jun 20 '11 at 17:20
  • This is a great answer if you can use javascript as it removes the need to even specify an initial height on your freezepane or a margin on the container which follows. I just shoved it in pageLoad() and add an event listener for page resizing. – Ravendarksky May 14 '15 at 14:25
  • either an event listener for page resizing, or a setInterval to ensure it is positioned correctly.. – ferr May 12 '17 at 15:17
  • 1
    I just had this same problem and I put code very similar to the above into a function called by That appears to work. In my case the lower div was already defined as position: relative so I set content.style.top instead of marginTop, which maybe there are pros and cons but same idea. – Jay Jun 09 '20 at 19:32
  • This is indeed a great answer with great example code. It would be even better with mention of and a sample of, attaching these two lines to an event listener that triggers when the height of head changes. Is that possible? – Bernd Wechner May 11 '21 at 02:36
27

Here's a solution, but it's a cheat. Basically, you have a duplicate header element, to push down the content, under the fixed position one:

<div class="outer">
    <div class="header">Header content goes here</div>
    <div class="header-push">Header content goes here</div>
    <div class="content">
        ...
    </div>
</div>
Eric
  • 95,302
  • 53
  • 242
  • 374
  • Thanks to @Eric for the main code, I just tweaked it a bit to make the header dynamic for the content. I just used jQuery to make the content stick to the height of the header no matter how big it gets. http://jsfiddle.net/K7h9k/5/ – Kyle Undefined Jun 20 '11 at 17:07
  • 1
    That's a nice (if a bit hacky) solution but I simplified my requirements to make the question easier to answer. I actually want two independent columns underneath so I can't use full page scrolling to do it. Maybe Javascript is the only way. I was hoping some of the new table layout stuff would do the trick but I've not been able to make it work. – David Roe Jun 20 '11 at 17:15
  • 2
    I've been trying for a few hours to come up with a pure-CSS solution, and concluded that indeed, it's not possible. The wonders of a standard invented in '96. – Dan Dascalescu Sep 03 '12 at 07:03
  • 1
    This is great. And if you don't want your HTML to get dirty, you can always make the copy using JavaScript. – Oleh Prypin Oct 13 '14 at 11:49
  • @OlehPrypin and unnecessarily make your site unusable to anyone who browses with javascript disabled? – Woodrow Barlow May 14 '15 at 15:07
  • 2
    @WoodrowBarlow That doesn't make the site unusable. The header will just scroll, not stay fixed. – Oleh Prypin May 14 '15 at 15:09
  • In some cases this can leave the scroll bar partially hidden underneath the fixed element. As far as I can tell, the only case where it doesn't hide the scroll bar is when the scrollable element is ``. – Chris Apr 14 '16 at 11:35
  • 1
    Don't forget about accessibility: If you can't just set `visibility: hidden` on the copy, at least use `aria-hidden="true"` to prevent the possibility of a screen reader thinking there are actually 2 instances of the text. – Tomty May 16 '20 at 14:27
11

I did a combination of both the accepted and Eric's answer. An empty div is used to push the content bellow "head". The width of this div is set by jQuery when window.onresize is fired:

function resizeHeader() {
    $(".header-push").height($(".header").height());
}
$(document).ready(resizeHeader);
$(window).resize(resizeHeader);

See this jsFiddle for more info.

geca
  • 2,711
  • 2
  • 17
  • 26
  • 2
    Oddly, I like this approach the best. It doesn't require a duplicate header (but it does require an empty div, which doesn't both me), and it doesn't require the content to know anything about the header. It's also much shorter. – Boom Jan 04 '16 at 20:27
  • 2
    Helpful to add a bit of a delay during window resize to avoid too many triggers - I use 250 ms and seems good enough: http://stackoverflow.com/questions/2854407/javascript-jquery-window-resize-how-to-fire-after-the-resize-is-completed – But those new buttons though.. Feb 06 '17 at 02:24
  • It took me way to much time to figure out a pure CSS solution isn't possible. This does the trick definitely, but what about people having JS disabled (although I can imagine they have a pretty bad browsing experience already probably)? – edwardmp Apr 06 '17 at 01:20
  • CSS is enormously complicated. There must exist clean, CSS-only solutions to questions like this. Somebody really intelligent and experienced with CSS should sit down and redesign the whole thing. It should be possible to do all the simple things one can imagine: create a div of fixed height, and one that fits to its content, calculate the true pixel distance of an element from another element, etc. If one specifies something with a clear meaning, like "min-height: 30rem", then the displayed height should never be something that violates this rule, such as 1rem, unless an error is thrown. – David Spector Aug 29 '19 at 12:48
  • 1
    We already have flexbox, which probably solves this problem easily. See https://css-tricks.com/snippets/css/a-guide-to-flexbox/ . – David Spector Aug 29 '19 at 12:59
6

The easiest way is to simply replace position: fixed; with position: sticky;

header {
  position: sticky;
  top:0;
  background-color: red;
  color: #fff;
}

main {
  background-color: green;
  color: #fff;
  min-height: 150vh;
}

DEMO: https://jsfiddle.net/4zndve6p/

DimChtz
  • 4,043
  • 2
  • 21
  • 39
3

This javascript will take the variable height of a fixed header and set the top margin of the content to flow underneath. Just call page load.

<script type="text/javascript">
    function AdjustHeight() {
        var height = document.getElementById("fixedheader").offsetHeight;
        document.getElementById("content").style.marginTop = height + 'px';
    }    
</script>
Keith
  • 61
  • 1
2

Here is a full code example of Shauna's solution above for the lazy/confused/people who can't get this working.

<html>
<head>

<style>
#FreezePaneHeader {
    position: fixed; top:0; left: 0; width: 100%;background-color: gray;
}

#FreezePaneBody {
    margin-top: 30px;
} 
</style>

 <script type="text/javascript">        
        function resizeFreezePane()
        {
            var height = document.getElementById("FreezePaneHeader").offsetHeight;
            document.getElementById("FreezePaneBody").style.marginTop = height + 'px';
        }

        //Resizes content to allow static header with dynamic height on postbacks
        function pageLoad() {
            resizeFreezePane();
        }

        var addEvent = function (elem, type, eventHandle) {
            if (elem == null || typeof (elem) == 'undefined') return;
            if (elem.addEventListener) {
                elem.addEventListener(type, eventHandle, false);
            } else if (elem.attachEvent) {
                elem.attachEvent("on" + type, eventHandle);
            } else {
                elem["on" + type] = eventHandle;
            }
        };

        //also when page is resized.
        addEvent(window, "resize", resizeFreezePane);

    function GenerateLorem(ele){
        var x = document.getElementById(ele);
        x.innerHTML = x.innerHTML + "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur pretium euismod leoquis convallis. In ornare suscipit massa eu commodo. Pellentesque vel nibh volutpat, commodo libero et, 

porta tellus. Duis eu fringilla arcu. Etiam imperdiet lectus diam, dignissim viverra dui hendrerit id. Fusce condimentum bibendum tincidunt. Aenean fringilla felis elit, et dapibus ante tempus at. Phasellus quis dolor odio.</p>";
        resizeFreezePane();
        }
    </script>
</head>

<body>
<div id="FreezePaneHeader">Frozen Page Header</div>
<div id="FreezePaneBody">
<button id="ButtonAddText" onclick="GenerateLorem('FreezePaneHeader'); return false;">Add more header text</button>
<button id="ButtonAddText" onclick="GenerateLorem('FreezePaneBody'); return false;">Add more body text</button>
<p id="PageContents"></p>
</div>
</body>

</html>
Ravendarksky
  • 573
  • 5
  • 13
0

I solved this problem using getComputedStyle. This method allows you to take all CSS styles. And based on them, change the style of another element. Here is all the code using native JS:

<style>
#head {
    position: fixed;
    width: 100%;
    top: 0;
    left: 0;
    background: rgb(170, 170, 170);
    /* height: 50px; for example no fixed height*/
    z-index: 2;
}

#content {
    position: relative;
    width: 100%;
    background: rgb(180, 37, 37);
  }
<body>
<div id="head">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Enim, quisquam.</div>
<div id="content">
    Some content text<br />

</div>
<script>
    const header = document.querySelector('#head'),
        content = document.querySelector('#content');
    //Now get all possible CSS styles from #head using getComputedStyle
    const marginTopOffset = getComputedStyle(header);
//Resize event is for autosearch width of a browser window
window.addEventListener('resize', ()=> {
    function marginOffset() {
        content.style.marginTop = marginTopOffset.height;
        // you can add changes to another CSS style 
        // content.style.backgroundColor = marginTopOffset.backgroundColor;
    };
    marginOffset();
    // You can see how changes margin-top in console on the fly
    console.log(content);
});
</script>
</body>
Antonio M
  • 1
  • 1
0

Here is my simple and working solution with just four lines of code.

Description

  • getBoundingClientRect calculates the effective properties of an element, including the margins. I use this to get the effective bottom value.
  • This value is assigned to the marginTop property of the content element.
  • An event listener for onResize is required to react to changes in the window size, which can effect the header height.

See it working

My fiddle (adjust width to see the effect): https://jsfiddle.net/z1uac3Lw/

function adjustHeaderHeight() {
   let header = document.getElementById('head');
   let rect = header.getBoundingClientRect();
   document.getElementById('content').style.marginTop = rect.bottom + 'px';
}

window.addEventListener('resize', function() {
   adjustHeaderHeight();
});
body {
    margin: 0;
}

#head {
    background: yellow;
//    height: 20px; /* I can't rely on knowing this. */
}

#content {
    background: red;
    position: absolute;
    top: 0px;
    bottom: 0;
    width: 100%;
    overflow: auto;
}
<!DOCTYPE html>
<html>
    <head>
    </head>
    <body onLoad='adjustHeaderHeight();' >
        <div id="head">some variable height content ... with a very long long long long long long long long long long long long long long long long long long long long long line to show the effect</div>
        <div id="content">
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
            scrollable content<br/>
        </div>
    </body>
</html>

Code with inline javascript / CSS

<!DOCTYPE html>
<html>
    <head>
        <style type="text/css">
body {
    margin: 0;
}

#head {
   background: yellow;
//    height: 20px; /* I can't rely on knowing this. */
}

#content {
   background: red;
   position: absolute;
   top: 0px;
   bottom: 0;
   width: 100%;
   overflow: auto;
}

        </style>
        <script type="text/javascript" type="text/javascript">        
    
function adjustHeaderHeight() {
   let header = document.getElementById('head');
   let rect = header.getBoundingClientRect();
   document.getElementById('content').style.marginTop = rect.bottom + 'px';
}

window.addEventListener('resize', function() {
   adjustHeaderHeight();
});

      </script>        
        
   </head>
   <body onLoad='adjustHeaderHeight();' >
      <div id="head">some variable height content ... with a very long long long long long long long long long long long long long long long long long long long long long line to show the effect</div>
      <div id="content">
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
         scrollable content<br/>
      </div>
   </body>
</html>
BogisW
  • 401
  • 2
  • 16