Go back

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.