Layout is the part of front-end work that feels like it should be solved by now. It mostly is — CSS Grid, flexbox, position: sticky and scroll snap cover almost everything a page needs — and yet the reflex when a design lands is still to install something. A grid framework for a card list that auto-fill already handles. A masonry library for a gallery that four lines of CSS can do. A resizable-panel package for a divider you can drag with about forty lines of JavaScript. The framework isn't wrong, exactly. It's just usually answering a question the browser stopped asking a few years ago.
Below are 10 free layout snippets you can preview live and copy in seconds: an app shell, a responsive card grid, a bento grid, masonry, a filterable portfolio, a split-screen landing page, a sticky sidebar with a live table of contents, full-page snap scrolling, drag-to-resize panels, and a virtual scroll list that handles ten thousand rows. Two of them are pure CSS with not a line of JavaScript, and two more use it only for a menu toggle or a touch fallback. The rest use it for behavior — filtering, position tracking, dragging, windowing — and none of them use it for positioning.
Each one comes with how it actually works, when to reach for it, and a tip for adapting it. They're plain HTML, CSS and vanilla JavaScript with no build step and no dependencies.
Every snippet below is a live, interactive preview — resize it, scroll it, drag it, right in the article. When you find one you like, hit View / Edit Code to grab the HTML/CSS/JS or export it to React, Vue, Angular or Tailwind.
What makes a layout hold up
Three ideas run through all ten, and they're worth naming up front because they're what separates a layout that survives real content from one that only works in the mockup:
- Let the container decide, not the breakpoint.
repeat(auto-fill, minmax(220px, 1fr)),columns: 3,flex: 1— each of these describes an intention ("cards at least 220px wide, fill the row"), not a pixel width at a screen size. Intentions survive a redesign; hard-coded breakpoints don't. - One function owns each piece of derived state. The resizable panels'
setSplit(), the virtual list'srender(), the filter grid'sfilterItems()— every interaction routes through a single function, so the layout can never end up in a state where two parts of the UI disagree about what's on screen. - The browser is better at layout math than you are. Masonry with CSS columns, snap points with
scroll-snap-type, equal-height columns with grid, position tracking withIntersectionObserver— every one of these replaces a scroll handler doing arithmetic ongetBoundingClientRect()with something the browser computes natively, off the main thread where it can.
1. Holy Grail Layout
The classic app shell: full-width header and footer, with a fixed nav, a fluid main column and a fixed aside between them — all three the same height, however much content each holds.
How it works: two nested grids, and that's the whole layout. The outer shell is grid-template-rows: auto 1fr auto on a min-height: 100vh container, which is what pins the footer to the bottom of short pages without any calc() or sticky-footer hack — the middle row simply absorbs whatever height is left. Inside it, grid-template-columns: 200px 1fr 220px gives the three-column row: fixed rails, fluid center. The equal-height columns that made this layout famously hard in the float era are now free, because grid items stretch to their row by default. The responsive behavior is two ordered retreats: at 860px the aside moves to grid-column: 1 / -1 and becomes a horizontal flex row under the content, and at 560px the nav leaves the flow entirely — position: fixed, transform: translateX(-100%), slid back in by an .open class. The JavaScript is two event listeners: one toggles that class and mirrors it into aria-expanded, and one closes the drawer when a link inside it is clicked — since leaving a nav open over the page you just navigated to is the most common mobile-menu bug there is.
Best for: dashboards, docs sites, admin panels — anything with persistent navigation on one side and contextual links on the other. Tip: the card area inside main uses repeat(auto-fill, minmax(140px, 1fr)), which means it reflows correctly on its own as the fluid column changes width — nest a container-driven grid inside a fixed-rail layout and you rarely need a second breakpoint.
Grab the code: Holy Grail Layout
2. Responsive Card Grid
The single most reusable layout on this list: cards that reflow from three columns to two to one without a media query anywhere.
How it works: the entire responsive behavior is grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)). Read it as a rule rather than a layout: every column is at least 220px, columns share leftover space equally, and the browser fits as many as it can. Narrow the container and a column drops out; widen it and one appears. No breakpoints, no JavaScript, no resize listener — and crucially, it responds to the container's width, not the viewport's, so the same grid works in a full-width page and inside a narrow sidebar with no changes. The cards themselves are ordinary: overflow: hidden with a border radius so the colored thumbnail clips to the rounded corner, and a hover state that lifts the card 2px while raising a soft shadow.
Best for: blog indexes, product listings, team pages, docs landing pages — the default answer whenever the design says "a grid of cards". Tip: swap auto-fill for auto-fit if you'd rather a small number of cards stretch to fill the row instead of leaving empty column tracks. That one word is the entire difference between the two, and it's the thing people most often get wrong.
Grab the code: Responsive Card Grid
3. Bento Grid
The asymmetric feature grid that Apple and Linear made the default look for product pages — cells of different widths, arranged so the important ones get more room.
How it works: a plain three-column grid — repeat(3, 1fr) with a 10px gap — where exactly two cells carry grid-column: span 2. That's the whole trick. The first cell spans two columns so its neighbor takes the remaining one, giving a 2+1 row. The next cell is a single, which pushes the following span-2 cell into columns two and three — so the second row reads 1+2 without a single line of code saying so. No grid-template-areas, no explicit row or column start values, no manual placement of any kind — the browser's auto-placement algorithm derives the whole asymmetric arrangement from two span declarations. Two cells get gradient backgrounds instead of the flat card surface, which is what stops the grid reading as a uniform card list; the rest inherit one shared .cell style. Every cell lifts 2px on hover and brightens its border, both on a short transition, and the whole thing is zero JavaScript.
Best for: feature sections, landing pages that need to preview a product without a screenshot, portfolio grids, and stat-heavy "why us" blocks. Tip: keep it to one span-2 cell per row and let auto-placement do the rest — the moment you start writing explicit grid-row values you've signed up for maintaining them at every breakpoint. For a uniform-cell version of the same idea, the Responsive Card Grid above is the simpler starting point.
Grab the code: Bento Grid
4. Masonry Grid
The Pinterest layout: cards of different heights packing into columns with no gaps — and no masonry library.
How it works: columns: 3 and column-gap: 12px on the container, plus break-inside: avoid on every card. That's the layout. CSS multi-column fills top-to-bottom within a column before moving to the next, which is exactly the masonry fill order, and it handles variable heights natively — no measuring, no absolute positioning, no reflow pass after images load, and no library recalculating anything on resize. break-inside: avoid is the one line people forget, and without it a tall card will happily split its top half into one column and its bottom half into the next. The cards are rendered from a JavaScript array so you can filter and sort them as data, and a media query drops to two columns on narrow screens.
The honest trade-off: multi-column is column-major, so with three columns the DOM order 1-2-3-4 reads down the left column, not across the top row. If the visual order has to match the source order — a ranked list, a feed where recency matters — this is the wrong tool and a JavaScript masonry library is the right one.
Best for: image galleries, moodboards, design showcases, testimonial walls — anywhere the arrangement is decorative rather than ranked. Pairs naturally with a photo gallery lightbox on click. Tip: the demo builds its cards with grid.innerHTML += inside a loop, which is fine for nine items but reparses the entire grid on every iteration. For a real gallery, build one HTML string and assign it once.
Grab the code: Masonry Grid
5. Portfolio Filter Grid
A category-filtered work grid: tabs with per-category counts, items that animate in as a staggered wave, and an empty state for categories with nothing in them.
How it works: each item carries a data-cat attribute and each tab a data-filter, and filtering is one pass that toggles a .hide class (display: none) based on whether they match. The interesting part is the entrance animation. Items animate in with a keyframe, but a CSS animation only runs once per element — re-showing a hidden card would normally just pop it back with no motion. The fix is the standard restart trick: remove the animation class, force a synchronous reflow by reading offsetWidth, then re-add it. Because the delay is computed from a counter that only increments for visible items (shown * 0.045s), the stagger is always a clean cascade across whatever's actually on screen, rather than inheriting gaps from the hidden ones. Items use aspect-ratio: 1, so the grid stays a perfect square lattice regardless of what's inside them, and a gradient overlay via ::after keeps the white caption legible over any thumbnail color.
Best for: portfolios, case study indexes, project galleries, and any "browse by category" listing. Tip: the tab counts are hardcoded in the markup here. When you wire it to real data, derive them from the items themselves — a count that says "Design 3" next to two visible cards is worse than no count at all. Consider pairing it with a proper empty state if a filter can genuinely return nothing.
Grab the code: Portfolio Filter Grid
6. Split Screen Layout
Two full-height halves for two audiences — and hovering either one lets it push the other aside.
How it works: both halves are flex children at flex: 1, so they split the viewport evenly at rest. The expand effect is two selectors working together: .sps:hover .sps-half { flex: 0.7 } shrinks both halves the moment the pointer enters the container, then .sps:hover .sps-half:hover { flex: 1.6 } re-expands only the one actually under the cursor. Putting the shrink on the parent's hover is what makes the effect symmetrical. A sibling selector can only reach forward — .sps-half:hover + .sps-half styles the half that comes after, never the one before it — so hovering the left half would shrink the right, and hovering the right half would do nothing at all. A transition: flex .5s with a sharp cubic-bezier does the animating, and the whole effect is scoped inside @media (min-width: 721px) so it never fires where it can't work. Below that, the halves stack vertically at min-height: 50vh each. The only JavaScript is a touch fallback: under matchMedia('(hover: none)'), tapping a half expands it — and taps that land on a button are ignored, so the calls to action keep working instead of being swallowed by the expand handler.
Best for: pricing or audience forks (personal vs. teams, buy vs. sell, students vs. professionals), and landing pages with exactly two equally-weighted paths. Tip: two halves is the limit for this pattern — with three, the hovered-vs-others ratio stops reading as a deliberate choice and starts reading as a bug. For more than two paths, use a split hero with a card row underneath instead.
Grab the code: Split Screen Layout
7. Sticky Sidebar
Long-form content with a sidebar that follows you down the page and highlights whichever section you're currently reading.
How it works: the layout is a two-column grid, and the sidebar is position: sticky; top: 20px. The line that makes it actually work is align-items: start on the grid (mirrored by align-self: start on the item) — without it, grid stretches the sidebar to the full height of the row, and a sticky element that already fills its container has nowhere to travel, which is the single most common reason "my sticky sidebar isn't sticking". The active-section tracking is an IntersectionObserver with rootMargin: '-20% 0px -70% 0px', which trims 20% off the top of the viewport and 70% off the bottom, leaving a band about a tenth of the screen tall in the upper third: a section counts as "current" only while it's passing through that band, which is a far better proxy for what someone is reading than "is any part of it visible". No scroll handler, no getBoundingClientRect() loop, no throttling to write. Sections carry scroll-margin-top so smooth-scrolled anchors don't land jammed against the top edge, and on narrow screens the sidebar drops to position: static below the content.
Best for: documentation, long articles, guides, changelogs — anything with headings a reader might want to jump between. Tip: the observer fires per intersecting section, so with very short sections two can qualify in quick succession and the highlight can flicker. If your sections are short, narrow the band instead — something like -30% 0px -69% leaves a strip barely 1% tall, which only one section can occupy at a time.
Grab the code: Sticky Sidebar
8. Full Page Scroll
Full-viewport sections that snap into place as you scroll, with a dot navigator that tracks your position and keyboard controls.
How it works: the snapping is entirely CSS — scroll-snap-type: y mandatory on the scroll container and scroll-snap-align: start on each 100vh section. That means the wheel, a trackpad swipe, a touch drag and a keyboard scroll all snap identically, because the browser owns the behavior rather than a hijacked scroll handler. This is the whole reason to prefer scroll snap over a fullpage library: hijacking scroll breaks momentum, breaks find-in-page, breaks the scrollbar, and breaks on every browser that updates its scrolling physics. The dot navigator is generated from the section list instead of hand-written markup, so adding a fifth section adds a fifth dot for free, and the active dot is driven by an IntersectionObserver scoped to the scroller with threshold: 0.6 — meaning a section only claims the dot once it's more than 60% visible. Because the observer watches the result rather than the input, the dot stays correct no matter how you got there: wheel, swipe, arrow keys, or a click on another dot. Each section's background is a --bg custom property set inline, so a new section needs one attribute instead of a new CSS rule.
Best for: product tours, launch pages, portfolios, and pitch-deck-style storytelling sites. Tip: mandatory snapping forces every scroll to land on a section boundary, which is wrong for any section taller than the viewport — if your content might overflow, switch to proximity, which only snaps when you're already close.
Grab the code: Full Page Scroll
9. Drag-to-Resize Split Panels
The editor-and-preview split every code playground has: a divider you can drag, double-click to reset, and move with the arrow keys.
How it works: the left pane has an explicit percentage width and the right pane is flex: 1, so only one number ever needs to change — set the left pane's width and the right one absorbs the remainder automatically. A single setSplit(pct) function is the only thing allowed to write that number: it clamps the value between an 18% minimum and its mirror at 82%, sets the width, and updates both percentage readouts in the same pass, so the two labels can never disagree. Every input calls it — the drag, the double-click reset, and the arrow keys, which nudge by 4% at a time and work at all because tabindex="0" makes the divider focusable, with role="separator" telling assistive tech what it is. The detail that makes the drag feel right is that mousemove and touchmove are bound to window, not to the divider: drag fast and your pointer will outrun a 14px-wide element constantly, and a listener attached to the divider itself would drop the gesture the moment that happened. user-select: none on the container stops the panes' text being selected mid-drag, and touch-action: none on the divider stops a touch drag being interpreted as a page scroll.
Best for: code playgrounds, editor-preview UIs, file browsers, diff views, and any tool where the ideal split depends on the task. Tip: the minimum is a percentage, which means on a very narrow screen 18% can still be too small to hold anything. If your panes have real content, clamp against a pixel floor as well and stack them vertically below a breakpoint.
Grab the code: Drag-to-Resize Split Panels
10. Virtual Scroll List
Ten thousand rows, scrolling smoothly, with only the rows you can actually see existing in the DOM — plus live search and sort.
How it works: three structural pieces. A scroll container with overflow-y: auto; an empty spacer div inside it whose height is set to items.length × ITEM_HEIGHT, which is what gives the scrollbar a correct range and thumb size as though all ten thousand rows existed; and a small pool of real row elements, absolutely positioned over that spacer at top = index × ITEM_HEIGHT. On scroll, the start index is Math.floor(scrollTop / ITEM_HEIGHT) and the count is the container height divided by the row height, plus a four-row buffer above and below so a fast fling never reveals a blank gap. The pool grows or shrinks to match — while (children.length < needed) append, while (> needed) remove — and existing rows are then rewritten in place, so scrolling mutates text content instead of rebuilding markup. Scroll events fire far more often than the screen refreshes, so renders are coalesced to one per frame with a requestAnimationFrame flag. Search filters the underlying array and resets the spacer to the new length, which shrinks the scrollbar to match the result count; sort reorders the array and repaints the same visible window. Both stay instant for the same reason: they operate on plain data, and the DOM cost is bounded by your viewport, not your dataset.
Best for: data grids, log viewers, contact and file lists, chat histories, and any table that could plausibly return thousands of rows. Tip: the entire technique depends on ITEM_HEIGHT being a real constant. If a row can wrap to two lines, the index math silently drifts and rows start landing in the wrong place — either enforce the height with overflow: hidden and ellipsis, or move to a measured, variable-height implementation. For a gentler version of the same problem, infinite scroll appends pages instead of windowing them.
Grab the code: Virtual Scroll List
How to drop these into your project
Every snippet is framework-agnostic, so the workflow is the same wherever you're building:
- Open the snippet and hit View / Edit Code to see the HTML, CSS and JS in separate tabs.
- Paste the HTML into your template, the CSS into your stylesheet, and the JS before your closing
</body>tag — none of these ten need a CDN script or a build step. - Prefer a component? Use the one-click export to React, Vue, Angular or Tailwind right from the snippet page.
- Replace the demo content first, before you touch the CSS. Most of what looks like layout code in these files is just the sample cards and their styling — the layout itself is usually a handful of declarations, and real content needs fewer changes than you'd expect.
- Check the two states people forget: what the layout does with one item, and what it does with fifty. Grids and masonry both look best in the demo's happy middle and worst at the extremes.
A few layout principles worth keeping
- Prefer intrinsic rules to breakpoints.
minmax(),auto-fill,flex: 1andcolumnsdescribe what you want; media queries describe where you gave up. Every breakpoint you don't write is one you don't maintain. - Give the browser the behavior, keep the decisions. Scroll snap, sticky positioning and
IntersectionObserverall replace scroll-handler math with something native, smoother and shorter. Your JavaScript should be deciding what is active, not calculating where things are. - One writer per number. The panels'
setSplit()and the virtual list'srender()are each the only function that writes their layout values. That's why neither can end up half-updated after a fast interaction. - Know each pattern's failure mode before you ship it. Masonry scrambles source order. Mandatory snap breaks on overflowing sections. Virtual scroll breaks on variable row heights. None of those are reasons to avoid the pattern — they're the questions to ask about your content first.
Final thought
These ten cover most of what a page needs structurally: a shell to hold it, a grid to list things in, a bento or masonry arrangement when uniform cards are too flat, filtering when there's too much to show at once, a sticky sidebar for long reads, and windowing when the list stops being a list and starts being a dataset. Start with whichever one matches the page you're actually building — the Responsive Card Grid is the one almost every project eventually needs, and it's three declarations.
Preview any of them live, tweak the code, and export to your framework of choice in one click. Browse the full collection at FWD Tools UI Snippets — it's free and runs entirely in your browser.
