How to make a full-page application?
Introduction
A full-page application is an interface with no page-level scrollbar: the entire content stays visible at all times, as on Figma or most dashboards designed to give an overview. Setting up such a layout is a fairly common task, but it hides a whole set of difficulties you need to account for. Here is a short collection of tips and pitfalls to avoid.
We will first look at a few common pitfalls when trying to build this kind of layout, then at approaches that can get you there.
Pitfalls to avoid
The temptation of 100vh
A common way to force an element to take the full height of the
page is to use a viewport-relative unit; more
specifically: vh.
For example, to make the content fill the full height of the
page, you might be tempted to use 100vh:
Code
<style> body { height: 100vh; background-color: aquamarine; }</style><body> <p>Contenu.</p></body>Résultat
The problem is that on mobile screens, the browser’s viewport is
dynamic (it has an address bar at the top and navigation buttons
at the bottom), while the vh unit is static and always
refers to the maximum possible height. That can lead to undesirable
results if you want an element to be exactly the height
of the screen.
One way to work around this is to use dynamic viewport units (svh and dvh), but since these are
relatively new units, you need to be careful if you want to keep
compatibility with older browsers. See dvh compatibility on CanIuse.
For better compatibility, I generally prefer to use height: 100%. Be careful though: the height property in CSS has a very specific behavior, and it is easy to
get lost if you do not understand how it works. I strongly recommend
this article by Josh Comeau on the topic.
Margins
Let’s say you have chosen the percentage approach for your full-page application, but you notice the element does not take the full height as expected:
Code
<style> html, body { height: 100%; }
body { margin: 8px; background-color: aquamarine; }</style><body> <p>Contenu.</p></body>Résultat
The offset comes from the body margins. Unlike padding
or the border, a margin is added outside the element’s box.
With height: 100 %, the browser therefore
computes 100 % height plus the margins, which causes
overflow and a scrollbar. Browsers apply a default margin of 8 px
on the body.
The most direct fix is to reset the margins to zero on
html and body. If you want to keep
inner spacing, prefer padding and make sure to use
box-sizing: border-box: without it, padding would
also be added to the total height, with the same unwanted
effect.
Code
<style> *, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body { padding: 8px; background-color: aquamarine; }</style><body> <p>Contenu.</p></body>Résultat
Here, the aquamarine background fills the page properly, with no
superfluous scrollbar. The 8 px padding is absorbed
thanks to border-box, and
height: 100 % locks the height to that of the viewport.
Practical case
Let’s look at a practical case together. The goal is to
reproduce the following structure: a fixed header, a filter
sidebar, and a main area with buttons and a diagram. Only the
content areas scroll internally; the body has no scrollbar.
To get there, two equivalent approaches exist; we will detail each
in its own subsection.
The height chain
Unlike the 100vh example above, we use
height: 100 % here. For that percentage to resolve,
you need an unbroken chain from the root:
html → body → …
So we start by explicitly setting the height on
html and body:
html,body { height: 100%;} Gotcha: without height: 100 %
on html, the percentage on body
resolves to nothing concrete and the container grows with its content.
If you use frameworks like React, you also need to apply height: 100 % on the mount element (#root, etc.) to extend this
chain down to the main component.
The body as a flex column
The body becomes a flex column container that takes
the full available height:
body { display: flex; flex-direction: column; gap: 1rem; padding: 16px;}
The 16 px padding is absorbed thanks to the
box-sizing: border-box declared earlier: it is included
in the 100 %, not added on top.
Gotcha: if the body has no fixed height,
it stretches with its content and becomes the scrolling element.
That is precisely what we want to avoid.
The main area
The header keeps its natural height. The
.content area gets flex: 1 to fill the
remaining vertical space, then becomes a row flex container to hold
the sidebar and the right-hand column:
.content { flex: 1; display: flex; gap: 1rem;}
.right-stuff { flex: 1; display: flex; flex-direction: column; gap: 1rem;}
.diagram { flex: 1;} Gotcha: the filter list has about thirty items.
Without further intervention, those items impose a minimum
height on their flex parents (default behavior:
min-height: auto) and the body ends up
overflowing.
Approach with min-height: 0
The first solution relies on a pair of properties, to place on every link of the flex chain down to the areas that should scroll:
.content,.filters,.right-stuff { min-height: 0;}
.filters,.diagram { overflow: auto;} min-height: 0 lets a flex child shrink below the size
of its content. Without it, flex: 1 is not enough: the
element refuses to shrink and pushes the whole page.
overflow: auto then moves scrolling to the right place:
the blue sidebar and the pink diagram area scroll internally, while
the body stays locked to the viewport height.
Gotcha: min-height: 0 alone clips the
content without offering a scrollbar;
overflow: auto alone on a parent with no constrained
height has no effect. The two properties work together.
Code
<style> *, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body { display: flex; flex-direction: column; gap: 1rem; padding: 16px; }
.content { flex: 1; display: flex; gap: 1rem; min-height: 0; }
.filters { overflow: auto; min-height: 0; }
.right-stuff { flex: 1; display: flex; flex-direction: column; gap: 1rem; min-height: 0; }
.diagram { flex: 1; overflow: auto; }</style><body> <header>HEADER</header> <div class="content"> <div class="filters">…</div> <div class="right-stuff"> <div class="toggles">…</div> <div class="diagram">…</div> </div> </div></body>Résultat
Approach with height: 100%
An alternative is to replace min-height: 0 with a chain
of height: 100 % on each link, complemented by
overflow: auto where needed:
.content,.filters,.right-stuff { height: 100%;}
.content,.filters,.diagram { overflow: auto;} height: 100 % propagates the parent’s height at
each level (.content, then
.filters or .right-stuff). However,
overflow: auto is not required on every link: only where
the element must both constrain itself and
scroll.
.content needs it because it is a flex child (flex: 1) of the body: without
overflow: auto (or min-height: 0), its
minimum height stays that of its content and the percentage
chain never starts. Once .content is constrained,
.right-stuff gets a resolved height via
height: 100 % alone; scrolling is then handled
by .diagram (flex: 1 + overflow: auto).
Gotcha: the rule “height: 100 %
+ overflow: auto on every link” looks appealing, but
overflow: auto on an intermediate container that does
not overflow itself (such as
.right-stuff) is superfluous. You need to
distinguish links that propagate height from those that
scroll.
Code
<style> *, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body { display: flex; flex-direction: column; gap: 1rem; padding: 16px; }
.content { flex: 1; display: flex; gap: 1rem; height: 100%; overflow: auto; }
.filters { overflow: auto; height: 100%; }
.right-stuff { flex: 1; display: flex; flex-direction: column; gap: 1rem; height: 100%; }
.diagram { flex: 1; overflow: auto; }</style><body> <header>HEADER</header> <div class="content"> <div class="filters">…</div> <div class="right-stuff"> <div class="toggles">…</div> <div class="diagram">…</div> </div> </div></body>Résultat
Both approaches produce the same visual result in this example.
min-height: 0 is the classic flex trick to allow shrinking;
height: 100 % can feel more intuitive if you are
already thinking in terms of a percentage chain from html and body.