Create a two-column layout with a fixed sidebar
Canonical URL: https://devexamples.com/css/create-a-two-column-layout-with-css-grid/
The problem
A documentation page pairs a persistent sidebar with a scrolling content column. The sidebar keeps one fixed design width, the content column must absorb every remaining pixel, and long unbreakable strings inside it must not push the page sideways. The same markup should stack into one column on narrow viewports without a wrapper rewrite.
Short answer
Declare grid-template-columns: 16rem minmax(0, 1fr) on the container, separate the columns with gap, then override the track list with a single minmax(0, 1fr) track in a max-width media query.
A page shell that pairs a persistent sidebar with a fluid content column is one grid track list, not a float hack or a percentage width on both children. Naming the tracks lets the sidebar hold an exact measure while the content column takes the remainder, and the same two rules can be reduced to one track when the viewport runs out of room.
<div class="site-layout">
<aside class="site-sidebar">
<h2>On this page</h2>
<ul>
<li><a href="#track-list">Track list</a></li>
<li><a href="#narrow-widths">Narrow widths</a></li>
<li><a href="#sticky-behaviour">Sticky behaviour</a></li>
</ul>
</aside>
<main class="site-content">
<h1>Two columns with CSS grid</h1>
<p>This column receives every pixel the sidebar does not claim.</p>
</main>
</div>.site-layout {
display: grid;
grid-template-columns: 16rem minmax(0, 1fr);
align-items: start;
gap: 2rem;
max-inline-size: 76rem;
margin-inline: auto;
padding: 1.5rem;
}
.site-sidebar {
position: sticky;
inset-block-start: 1.5rem;
max-block-size: calc(100dvh - 3rem);
overflow: auto;
padding-inline-end: 1rem;
border-inline-end: 1px solid var(--de-border, #c7ccd4);
overflow-wrap: break-word;
}
@media (max-width: 47.9375rem) {
.site-layout {
grid-template-columns: minmax(0, 1fr);
}
.site-sidebar {
position: static;
max-block-size: none;
padding-inline-end: 0;
border-inline-end: 0;
}
}Explanation
The container declares exactly two tracks, so the split is a property of the parent rather than a width claimed by each child. The first track is a hard 16rem: it never yields space, and it scales with the reader’s root font size because the unit is rem, which is what you want from a design token that also has to work at 200 percent zoom. The second track is minmax(0, 1fr) rather than a bare 1fr, and that zero is the whole reason the layout behaves. A single fr is shorthand for minmax(auto, 1fr), and auto as a minimum resolves to the item’s automatic minimum size, so one long unbreakable string in a code block or a URL sets a floor under the track and the grid becomes wider than its container. With the minimum pinned to zero, the track can never exceed the space left after the sidebar and the gap, and overflow stays inside the column where overflow-wrap: break-word can act on it.
The remaining declarations are choices about who owns each piece of space. gap carries the column separation so neither region needs a margin, which also means the stacked case needs no margin reset for the gap. align-items: start makes each item take its content height instead of stretching to the row; that is what allows the sidebar to be sticky, because a stretched item is already as tall as the row it travels in and has no room left to move. max-inline-size with margin-inline: auto caps line length on wide displays. Logical properties (max-inline-size, margin-inline, padding-inline-end, inset-block-start) keep the same rules correct for right-to-left content without a second block of mirrored values. The 100dvh cap plus overflow: auto on the sidebar bounds its own height against the dynamic viewport, so a long table of contents scrolls inside its box instead of stretching the row.
Two boundary conditions are worth knowing before this ships. First, the breakpoint comes from arithmetic rather than taste: the 16rem track, the 2rem gap and 3rem of horizontal padding mean that at 48rem of viewport the content column is about 27rem wide, and below that prose lines get short enough to hurt scanning, so the single-track override fires just before the 48rem mark. Second, when the override applies, both regions become full-width rows in source order, which means the sidebar’s links are read and tabbed through before the main content. Fixing that with order or with grid placement that moves a region away from its source position only creates a mismatch between visual and reading order, so the safe options are to accept the nav-first stack or to hide the sidebar behind a disclosure control at that width.
Usage notes
- Keep the sidebar width in exactly one declaration; repeating it as a max-inline-size on the aside gives you two numbers to change.
- If the sidebar may be narrower than 16rem, use minmax(min-content, 16rem) and drop the media query instead of adding a second breakpoint.
- The sticky sidebar needs align-items: start on the container; a stretched grid item is already row-height and has nothing left to travel.
Caveats
- The breakpoint is written in rem, so it moves with the user's root font size as well as with the viewport: a large base font reaches the stacked layout on a wide screen.
- A sticky sidebar silently changes behaviour if any ancestor between it and the page scrollport sets overflow; see the sticky table header Example for that rule.
Alternatives
Grid names the two tracks you actually mean, so a hard 16rem column and a fluid column survive long unbreakable content with one extra argument. Flexbox needs a flex-basis, a shrink factor and overflow handling to describe the same split, which makes it the better tool for centring one item rather than for a measured two-column frame.
Related examples
Editorial links first, then deterministic same-task or same-topic candidates. Tags and shared language alone never qualify a candidate.
Alternative approach
Centre an element with flexbox and no wrapper markupFlexbox reaches the same visual split without a track list when the sidebar can be content-sized.
Same task or topic
Build a card grid that reflows without media queriesSolves the Build a responsive layout task
Same task or topic
Keep a table header visible with sticky positioningSolves the Build a responsive layout task
References
- CSS grid layout guide(opens in a new tab) — MDN Web Docs. Explains track lists, implicit tracks and the fr unit used by the two-column declaration.
- CSS Grid Layout Module Level 1(opens in a new tab) — W3C. Normative track sizing algorithm behind the auto minimum size that 1fr inherits.
- minmax() - CSS | MDN(opens in a new tab) — MDN Web Docs. Defines the minimum and maximum track arguments and their effect on track overflow.
Source page: https://devexamples.com/css/create-a-two-column-layout-with-css-grid/