Layout beyond the grid: positioning and spacing recipes
Build page shells with flex utilities, keep headers sticky without breaking scroll, and use positioning and viewport sizing deliberately.
Page shells without custom CSS
The grid handles content columns, but the outer frame of a page β a header, a sidebar, a scrolling main area β is a flex problem. Bootstrap ships the flex utilities for exactly this, and they compose better than hand-written CSS because they stay consistent with the spacing scale.
<div class="d-flex flex-column min-vh-100">
<header class="navbar navbar-expand bg-body border-bottom flex-shrink-0">
<div class="container-fluid"><a class="navbar-brand" href="/">Acme</a></div>
</header>
<div class="d-flex flex-grow-1 overflow-hidden">
<aside class="border-end p-3 flex-shrink-0 d-none d-lg-block" style="width: 260px;">
<nav class="nav flex-column gap-1">
<a class="nav-link" href="#">Overview</a>
<a class="nav-link" href="#">Reports</a>
</nav>
</aside>
<main class="flex-grow-1 overflow-auto p-4">
<h1 class="h3">Overview</h1>
<p class="text-body-secondary">Scrollable content region.</p>
</main>
</div>
</div>min-vh-100makes the shell at least as tall as the viewport, so a short page still fills the screen.flex-grow-1on the middle row is what actually pushes the footer down; without it the row collapses to its content.overflow-hiddenon the row plusoverflow-autoonmainis what makes only the content scroll.flex-shrink-0stops narrow viewports from squashing a sidebar or header to nothing.
π‘
Bootstrap's flex utilities map directly to CSS
flex properties, so a browser with a working inspector is the fastest reference: the compiled rule for flex-grow-1 is literally flex-grow: 1 !important.Sticky versus fixed
| Utility | Behaviour | Fits |
|---|---|---|
position-static | Normal flow | The default; reset when a component forces positioning |
position-relative | Flow preserved, offsets apply | Anchor for absolutely positioned children |
position-absolute | Removed from flow | Badges, close buttons, overlays inside a relative parent |
position-fixed | Relative to the viewport | App bars, floating action buttons |
position-sticky | Relative until an offset is reached | Table headers, sticky sub-navigation |
<!-- sticky sub-nav inside a scrolling page -->
<div class="position-sticky top-0 bg-body py-2 border-bottom" style="z-index: 1020;">
<span class="text-body-secondary small">Section navigation</span>
</div>
<!-- sticky table header inside a scroll container -->
<div class="table-responsive" style="max-height: 380px; overflow-y: auto;">
<table class="table table-sm mb-0">
<thead class="position-sticky top-0 bg-body"><tr><th scope="col">Order</th><th scope="col">Total</th></tr></thead>
<tbody><tr><td>#1041</td><td>82.00</td></tr></tbody>
</table>
</div>position-stickyonly works when no ancestor setsoverflow: hidden,autoorscrollβ that is the single most common reason sticky silently does nothing.- A sticky element stays inside its parent box, so a header stuck inside a short column stops sticking where that column ends.
- Bootstrap's components set their own
z-index: the navbar uses 1030, modals 1055, tooltips 1080. Pick a value that fits that ladder instead of guessing 9999.
<div class="position-relative">
<button type="button" class="btn btn-primary">Inbox</button>
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill text-bg-danger">
9<span class="visually-hidden">unread messages</span>
</span>
</div>Viewport units, spacing and overflow
/* Bootstrap gives you the building blocks; a page-specific rule is still the
right tool when you need a literal value. */
.hero {
min-height: 60vh; /* NOT min-vh-60: Bootstrap has no such class */
margin-top: calc(-1 * var(--bs-navbar-height, 3.5rem));
}
.kanban {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 1rem;
}| Need | Use | Note |
|---|---|---|
| Height of the viewport | vh-100 | Makes the whole page scroll on mobile; prefer min-vh-100 |
| Width of the viewport | vw-100 | Overflows when a scrollbar is present |
| Relative to the containing block | h-100, w-100 | Requires the parent to have a resolvable height |
| Page padding | p-3 p-md-4 | Spacing scale is 0-5 plus auto |
| Gaps in a flex or grid row | gap-3 | Preferred over margins on children β no collapsing surprises |
Scrollbars are the hidden cost of viewport units: 100vw includes the scrollbar gutter while 100% does not, so a full-bleed banner built with vw-100 commonly produces a few pixels of horizontal scroll. Use w-100 on a full-width parent instead.
FAQ
Why does my sticky navbar jump when it sticks?
Something above it is collapsing β usually a margin that the sticky element was relying on, or the parent being a flex container with
align-items: stretch. Give the navbar a fixed context with position-relative on its wrapper, or move the sticky class onto the wrapper rather than the inner navbar.Should I use sticky or fixed for a header?
Sticky when the header should occupy space in the document (the normal case, and it avoids the padding hack). Fixed when the header must be outside normal flow entirely, for example on top of a full-bleed hero β then you must compensate with padding on the first section.
Related
The grid system Building a complete page with Bootstrap
Last refreshed 2026-09-18.