17 CSS Sticky Elements14 / 17

CSS onlyMIT licensed

CSS Sticky Footer to Bottom of Screen

The OTHER sticky footer — the one that predates position: sticky and still fills support forums: on a short page the footer floats awkwardly mid-screen; you want it pinned to the viewport bottom, but pushed down naturally once content grows. The 2026 answer is three lines of grid. This demo has a Short page / Long page toggle (a checkbox and :has(), no JS) so you can watch the same three lines handle both cases.

Published

Live Demo
Try it

The code

<section class="cst-14" aria-label="Sticky footer at bottom of screen demo">
  <div class="cst-14__stage" id="cst14-top">
    <div class="cst-14__page">
      <header class="cst-14__head">
        <a class="cst-14__brand" href="#cst14-top">Ledger <span>release notes</span></a>
        <fieldset class="cst-14__toggle">
          <legend class="cst-14__vh">Demo page length</legend>
          <input type="radio" name="cst14len" id="cst14-short" checked><label for="cst14-short">Short page</label>
          <input type="radio" name="cst14len" id="cst14-long"><label for="cst14-long">Long page</label>
        </fieldset>
      </header>
      <main class="cst-14__main">
        <p class="cst-14__explain">Toggle above. <strong>Short:</strong> content ends mid-screen, yet the footer holds the bottom edge — the <code>1fr</code> row absorbs the slack. <strong>Long:</strong> the same three lines of grid let the footer flow naturally after the content. No <code>position: fixed</code>, no JavaScript.</p>
        <article class="cst-14__note">
          <header><span class="cst-14__chip">v4.2.0</span><time>Jul 30, 2026</time></header>
          <h2>Saved views, shareable</h2>
          <ul><li>Any filtered ledger view now mints a URL — send it, bookmark it, embed it.</li><li>Export respects the active view, including hidden columns.</li><li>Fixed: month-end close banner no longer overlaps the totals row on 13-inch screens.</li></ul>
        </article>
        <div class="cst-14__extra">
          <article class="cst-14__note">
            <header><span class="cst-14__chip">v4.1.2</span><time>Jul 18, 2026</time></header>
            <h2>Quieter reconciliation</h2>
            <ul><li>Auto-match confidence now shown inline instead of a modal per match.</li><li>Keyboard: <kbd>J</kbd>/<kbd>K</kbd> walk unmatched entries; <kbd>A</kbd> accepts.</li></ul>
          </article>
          <article class="cst-14__note">
            <header><span class="cst-14__chip">v4.1.1</span><time>Jul 09, 2026</time></header>
            <h2>Patch: timezone sanity</h2>
            <ul><li>Statements imported from AU/NZ banks no longer time-travel a day.</li><li>Fixed a sticky table header (yes, we read demo 09) losing its shadow in Safari.</li></ul>
          </article>
          <article class="cst-14__note">
            <header><span class="cst-14__chip">v4.1.0</span><time>Jun 27, 2026</time></header>
            <h2>Rules engine, rebuilt</h2>
            <ul><li>Rules compose now — 'coffee AND &gt; $8' finally files as “regret”.</li><li>Dry-run mode previews the next import against your rule set.</li><li>14 new merchant normalizers for UK and Canadian banks.</li></ul>
          </article>
          <article class="cst-14__note">
            <header><span class="cst-14__chip">v4.0.2</span><time>Jun 14, 2026</time></header>
            <h2>Small sharp fixes</h2>
            <ul><li>CSV import handles files with BOM markers from Excel.</li><li>Dark mode: charts no longer glow radioactive green on P3 displays.</li></ul>
          </article>
          <article class="cst-14__note">
            <header><span class="cst-14__chip">v4.0.0</span><time>May 30, 2026</time></header>
            <h2>Ledger 4</h2>
            <ul><li>Multi-entity books, one login.</li><li>Realtime collaboration with per-cell presence.</li><li>New keyboard-first navigation across every screen.</li></ul>
          </article>
        </div>
      </main>
      <footer class="cst-14__footer">
        <div class="cst-14__cols">
          <div><strong>Product</strong><a href="#cst14-top">Pricing</a><a href="#cst14-top">Changelog</a><a href="#cst14-top">Roadmap</a></div>
          <div><strong>Company</strong><a href="#cst14-top">About</a><a href="#cst14-top">Blog</a><a href="#cst14-top">Careers</a></div>
          <div><strong>Support</strong><a href="#cst14-top">Docs</a><a href="#cst14-top">Status</a><a href="#cst14-top">Contact</a></div>
        </div>
        <div class="cst-14__legal"><span><i aria-hidden="true"></i>All systems normal</span><p>© 2026 Ledger — a CodeFronts sticky-elements demo. This footer is pinned by a 1fr grid row, not position: sticky.</p></div>
      </footer>
    </div>
  </div>
</section>
.cst-14,
.cst-14 *,
.cst-14 *::before,
.cst-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cst-14 {
  --bg: oklch(0.975 0.004 230);
  --ink: oklch(0.23 0.015 240);
  --mut: oklch(0.51 0.012 240);
  --line: oklch(0.9 0.007 235);
  --acc: oklch(0.55 0.15 220);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
  container-type: inline-size;
  display: block;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  background: var(--bg);
}

.cst-14__stage {
  width: 100%;
  container: cst14/inline-size;
  height: 100vh;
  height: 100svh;
  overflow-y: auto;
  background: var(--bg);
}

.cst-14__page {
  min-height: 100%;
  display: grid;
  grid-template-rows: auto 1fr auto;
}

.cst-14__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 20px;
  padding: 16px clamp(16px,4cqi,40px);
  border-bottom: 1px solid var(--line);
}

.cst-14__brand {
  font-size: 16px;
  font-weight: 800;
  letter-spacing: -.02em;
  color: inherit;
  text-decoration: none;
}

.cst-14__brand span {
  font-weight: 500;
  color: var(--mut);
}

.cst-14__brand:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 3px;
}

.cst-14__toggle {
  display: flex;
  border: 1.5px solid var(--line);
  border-radius: 12px;
  padding: 3px;
  background: oklch(1 0 0);
  gap: 2px;
}

.cst-14__vh {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.cst-14__toggle input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.cst-14__toggle label {
  display: grid;
  place-items: center;
  min-height: 38px;
  padding: 0 15px;
  border-radius: 9px;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--mut);
  cursor: pointer;
  transition: background .2s,color .2s;
}

.cst-14__toggle label:hover {
  color: var(--ink);
}

.cst-14__toggle input:checked+label {
  background: var(--ink);
  color: oklch(0.98 0 0);
}

.cst-14__toggle input:focus-visible+label {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

.cst-14__main {
  padding: clamp(22px,4cqi,40px) clamp(16px,4cqi,40px);
  display: grid;
  gap: 16px;
  align-content: start;
  max-width: 820px;
}

.cst-14__explain {
  font-size: 13.5px;
  line-height: 1.65;
  color: var(--mut);
  padding: 14px 16px;
  border-radius: 14px;
  background: oklch(0.93 0.015 225/.6);
  border: 1px solid color-mix(in oklch,var(--acc) 22%,var(--line));
}

.cst-14__explain strong {
  color: var(--ink);
}

.cst-14__explain code {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: .9em;
  background: oklch(1 0 0/.7);
  padding: 1px 5px;
  border-radius: 4px;
  color: var(--ink);
}

.cst-14__note {
  padding: 20px 22px;
  border-radius: 16px;
  background: oklch(1 0 0);
  border: 1px solid var(--line);
}

.cst-14__note header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}

.cst-14__chip {
  font-family: ui-monospace,Menlo,monospace;
  font-size: 11.5px;
  font-weight: 700;
  color: var(--acc);
  background: oklch(0.93 0.03 220/.7);
  padding: 3px 9px;
  border-radius: 99px;
}

.cst-14__note time {
  font-size: 12px;
  color: var(--mut);
}

.cst-14__note h2 {
  font-size: 17px;
  letter-spacing: -.015em;
  font-weight: 750;
  margin-bottom: 10px;
}

.cst-14__note ul {
  list-style: none;
  display: grid;
  gap: 7px;
}

.cst-14__note li {
  position: relative;
  padding-left: 18px;
  font-size: 13.5px;
  line-height: 1.6;
  color: oklch(0.38 0.015 240);
}

.cst-14__note li::before {
  content: "";
  position: absolute;
  left: 2px;
  top: 9px;
  width: 6px;
  height: 6px;
  border-radius: 2px;
  background: var(--acc);
}

.cst-14__note kbd {
  font-family: ui-monospace,Menlo,monospace;
  font-size: .85em;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 5px;
  padding: 1px 5px;
  background: oklch(0.97 0.004 230);
}

.cst-14__extra {
  display: none;
}

.cst-14__stage:has(#cst14-long:checked) .cst-14__extra {
  display: grid;
  gap: 16px;
}

.cst-14__footer {
  background: oklch(0.24 0.02 245);
  color: oklch(0.9 0.008 235);
}

.cst-14__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(min(150px,100%),1fr));
  gap: 20px;
  padding: 28px clamp(16px,4cqi,40px) 20px;
  max-width: 820px;
}

.cst-14__cols strong {
  display: block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: oklch(0.66 0.015 235);
  margin-bottom: 6px;
}

.cst-14__cols a {
  display: flex;
  align-items: center;
  min-height: 34px;
  font-size: 13px;
  font-weight: 600;
  color: oklch(0.88 0.01 235);
  text-decoration: none;
  width: fit-content;
}

.cst-14__cols a:hover,
.cst-14__cols a:focus-visible {
  color: oklch(0.99 0 0);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.cst-14__cols a:focus-visible {
  outline: 2px solid oklch(0.7 0.12 220);
  outline-offset: 2px;
  border-radius: 4px;
}

.cst-14__legal {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 22px;
  padding: 14px clamp(16px,4cqi,40px) 20px;
  border-top: 1px solid oklch(0.34 0.02 245);
}

.cst-14__legal span {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 12px;
  font-weight: 700;
  color: oklch(0.8 0.09 155);
}

.cst-14__legal i {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: oklch(0.75 0.16 155);
  box-shadow: 0 0 0 3px oklch(0.75 0.16 155/.25);
}

.cst-14__legal p {
  font-size: 11.5px;
  color: oklch(0.66 0.015 235);
}

@media (prefers-reduced-motion: reduce) {
  .cst-14 * {
    transition-duration: .01ms !important;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Sticky Element from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Sticky Footer to Bottom of Screen
Source: https://codefronts.com/layouts/css-sticky-elements/css-sticky-footer-to-bottom-of-screen/

The OTHER sticky footer — the one that predates position: sticky and still fills support forums: on a short page the footer floats awkwardly mid-screen; you want it pinned to the viewport bottom, but pushed down naturally once content grows. The 2026 answer is three lines of grid. This demo has a Short page / Long page toggle (a checkbox and :has(), no JS) so you can watch the same three lines handle both cases.
## HTML
```html
<section class="cst-14" aria-label="Sticky footer at bottom of screen demo">
  <div class="cst-14__stage" id="cst14-top">
    <div class="cst-14__page">
      <header class="cst-14__head">
        <a class="cst-14__brand" href="#cst14-top">Ledger <span>release notes</span></a>
        <fieldset class="cst-14__toggle">
          <legend class="cst-14__vh">Demo page length</legend>
          <input type="radio" name="cst14len" id="cst14-short" checked><label for="cst14-short">Short page</label>
          <input type="radio" name="cst14len" id="cst14-long"><label for="cst14-long">Long page</label>
        </fieldset>
      </header>
      <main class="cst-14__main">
        <p class="cst-14__explain">Toggle above. <strong>Short:</strong> content ends mid-screen, yet the footer holds the bottom edge — the <code>1fr</code> row absorbs the slack. <strong>Long:</strong> the same three lines of grid let the footer flow naturally after the content. No <code>position: fixed</code>, no JavaScript.</p>
        <article class="cst-14__note">
          <header><span class="cst-14__chip">v4.2.0</span><time>Jul 30, 2026</time></header>
          <h2>Saved views, shareable</h2>
          <ul><li>Any filtered ledger view now mints a URL — send it, bookmark it, embed it.</li><li>Export respects the active view, including hidden columns.</li><li>Fixed: month-end close banner no longer overlaps the totals row on 13-inch screens.</li></ul>
        </article>
        <div class="cst-14__extra">
          <article class="cst-14__note">
            <header><span class="cst-14__chip">v4.1.2</span><time>Jul 18, 2026</time></header>
            <h2>Quieter reconciliation</h2>
            <ul><li>Auto-match confidence now shown inline instead of a modal per match.</li><li>Keyboard: <kbd>J</kbd>/<kbd>K</kbd> walk unmatched entries; <kbd>A</kbd> accepts.</li></ul>
          </article>
          <article class="cst-14__note">
            <header><span class="cst-14__chip">v4.1.1</span><time>Jul 09, 2026</time></header>
            <h2>Patch: timezone sanity</h2>
            <ul><li>Statements imported from AU/NZ banks no longer time-travel a day.</li><li>Fixed a sticky table header (yes, we read demo 09) losing its shadow in Safari.</li></ul>
          </article>
          <article class="cst-14__note">
            <header><span class="cst-14__chip">v4.1.0</span><time>Jun 27, 2026</time></header>
            <h2>Rules engine, rebuilt</h2>
            <ul><li>Rules compose now — 'coffee AND &gt; $8' finally files as “regret”.</li><li>Dry-run mode previews the next import against your rule set.</li><li>14 new merchant normalizers for UK and Canadian banks.</li></ul>
          </article>
          <article class="cst-14__note">
            <header><span class="cst-14__chip">v4.0.2</span><time>Jun 14, 2026</time></header>
            <h2>Small sharp fixes</h2>
            <ul><li>CSV import handles files with BOM markers from Excel.</li><li>Dark mode: charts no longer glow radioactive green on P3 displays.</li></ul>
          </article>
          <article class="cst-14__note">
            <header><span class="cst-14__chip">v4.0.0</span><time>May 30, 2026</time></header>
            <h2>Ledger 4</h2>
            <ul><li>Multi-entity books, one login.</li><li>Realtime collaboration with per-cell presence.</li><li>New keyboard-first navigation across every screen.</li></ul>
          </article>
        </div>
      </main>
      <footer class="cst-14__footer">
        <div class="cst-14__cols">
          <div><strong>Product</strong><a href="#cst14-top">Pricing</a><a href="#cst14-top">Changelog</a><a href="#cst14-top">Roadmap</a></div>
          <div><strong>Company</strong><a href="#cst14-top">About</a><a href="#cst14-top">Blog</a><a href="#cst14-top">Careers</a></div>
          <div><strong>Support</strong><a href="#cst14-top">Docs</a><a href="#cst14-top">Status</a><a href="#cst14-top">Contact</a></div>
        </div>
        <div class="cst-14__legal"><span><i aria-hidden="true"></i>All systems normal</span><p>© 2026 Ledger — a CodeFronts sticky-elements demo. This footer is pinned by a 1fr grid row, not position: sticky.</p></div>
      </footer>
    </div>
  </div>
</section>
```
## CSS
```css
.cst-14,
.cst-14 *,
.cst-14 *::before,
.cst-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cst-14 {
  --bg: oklch(0.975 0.004 230);
  --ink: oklch(0.23 0.015 240);
  --mut: oklch(0.51 0.012 240);
  --line: oklch(0.9 0.007 235);
  --acc: oklch(0.55 0.15 220);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
  container-type: inline-size;
  display: block;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  background: var(--bg);
}

.cst-14__stage {
  width: 100%;
  container: cst14/inline-size;
  height: 100vh;
  height: 100svh;
  overflow-y: auto;
  background: var(--bg);
}

.cst-14__page {
  min-height: 100%;
  display: grid;
  grid-template-rows: auto 1fr auto;
}

.cst-14__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 20px;
  padding: 16px clamp(16px,4cqi,40px);
  border-bottom: 1px solid var(--line);
}

.cst-14__brand {
  font-size: 16px;
  font-weight: 800;
  letter-spacing: -.02em;
  color: inherit;
  text-decoration: none;
}

.cst-14__brand span {
  font-weight: 500;
  color: var(--mut);
}

.cst-14__brand:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 3px;
}

.cst-14__toggle {
  display: flex;
  border: 1.5px solid var(--line);
  border-radius: 12px;
  padding: 3px;
  background: oklch(1 0 0);
  gap: 2px;
}

.cst-14__vh {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.cst-14__toggle input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.cst-14__toggle label {
  display: grid;
  place-items: center;
  min-height: 38px;
  padding: 0 15px;
  border-radius: 9px;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--mut);
  cursor: pointer;
  transition: background .2s,color .2s;
}

.cst-14__toggle label:hover {
  color: var(--ink);
}

.cst-14__toggle input:checked+label {
  background: var(--ink);
  color: oklch(0.98 0 0);
}

.cst-14__toggle input:focus-visible+label {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

.cst-14__main {
  padding: clamp(22px,4cqi,40px) clamp(16px,4cqi,40px);
  display: grid;
  gap: 16px;
  align-content: start;
  max-width: 820px;
}

.cst-14__explain {
  font-size: 13.5px;
  line-height: 1.65;
  color: var(--mut);
  padding: 14px 16px;
  border-radius: 14px;
  background: oklch(0.93 0.015 225/.6);
  border: 1px solid color-mix(in oklch,var(--acc) 22%,var(--line));
}

.cst-14__explain strong {
  color: var(--ink);
}

.cst-14__explain code {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: .9em;
  background: oklch(1 0 0/.7);
  padding: 1px 5px;
  border-radius: 4px;
  color: var(--ink);
}

.cst-14__note {
  padding: 20px 22px;
  border-radius: 16px;
  background: oklch(1 0 0);
  border: 1px solid var(--line);
}

.cst-14__note header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}

.cst-14__chip {
  font-family: ui-monospace,Menlo,monospace;
  font-size: 11.5px;
  font-weight: 700;
  color: var(--acc);
  background: oklch(0.93 0.03 220/.7);
  padding: 3px 9px;
  border-radius: 99px;
}

.cst-14__note time {
  font-size: 12px;
  color: var(--mut);
}

.cst-14__note h2 {
  font-size: 17px;
  letter-spacing: -.015em;
  font-weight: 750;
  margin-bottom: 10px;
}

.cst-14__note ul {
  list-style: none;
  display: grid;
  gap: 7px;
}

.cst-14__note li {
  position: relative;
  padding-left: 18px;
  font-size: 13.5px;
  line-height: 1.6;
  color: oklch(0.38 0.015 240);
}

.cst-14__note li::before {
  content: "";
  position: absolute;
  left: 2px;
  top: 9px;
  width: 6px;
  height: 6px;
  border-radius: 2px;
  background: var(--acc);
}

.cst-14__note kbd {
  font-family: ui-monospace,Menlo,monospace;
  font-size: .85em;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 5px;
  padding: 1px 5px;
  background: oklch(0.97 0.004 230);
}

.cst-14__extra {
  display: none;
}

.cst-14__stage:has(#cst14-long:checked) .cst-14__extra {
  display: grid;
  gap: 16px;
}

.cst-14__footer {
  background: oklch(0.24 0.02 245);
  color: oklch(0.9 0.008 235);
}

.cst-14__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(min(150px,100%),1fr));
  gap: 20px;
  padding: 28px clamp(16px,4cqi,40px) 20px;
  max-width: 820px;
}

.cst-14__cols strong {
  display: block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: oklch(0.66 0.015 235);
  margin-bottom: 6px;
}

.cst-14__cols a {
  display: flex;
  align-items: center;
  min-height: 34px;
  font-size: 13px;
  font-weight: 600;
  color: oklch(0.88 0.01 235);
  text-decoration: none;
  width: fit-content;
}

.cst-14__cols a:hover,
.cst-14__cols a:focus-visible {
  color: oklch(0.99 0 0);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.cst-14__cols a:focus-visible {
  outline: 2px solid oklch(0.7 0.12 220);
  outline-offset: 2px;
  border-radius: 4px;
}

.cst-14__legal {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 22px;
  padding: 14px clamp(16px,4cqi,40px) 20px;
  border-top: 1px solid oklch(0.34 0.02 245);
}

.cst-14__legal span {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 12px;
  font-weight: 700;
  color: oklch(0.8 0.09 155);
}

.cst-14__legal i {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: oklch(0.75 0.16 155);
  box-shadow: 0 0 0 3px oklch(0.75 0.16 155/.25);
}

.cst-14__legal p {
  font-size: 11.5px;
  color: oklch(0.66 0.015 235);
}

@media (prefers-reduced-motion: reduce) {
  .cst-14 * {
    transition-duration: .01ms !important;
  }
}
```

How this works

Terminology first, because search engines blur two different patterns: this is NOT position:sticky. A 'sticky footer' in the classic sense is a layout guarantee — footer at the viewport bottom at minimum, pushed further by content — and it's solved by giving the page a minimum height and letting a middle row absorb the slack:

.page{ min-height:100svh;               /* fill the screen     */
  display:grid;
  grid-template-rows:auto 1fr auto; }   /* header main footer  */

/* flexbox spelling of the same idea: */
.page{ min-height:100svh; display:flex; flex-direction:column; }
footer{ margin-top:auto; }

The 1fr row is the entire mechanism: on a short page it stretches, shoving the footer to the bottom edge; on a long page it's already taller than the viewport, so the footer sits in normal flow and scrolls like anything else. No fixed positioning, no JS measuring, no height:100% chains through every ancestor (the old hack this replaces). 100svh — small viewport height — is deliberate: plain 100vh on mobile includes the browser chrome, guaranteeing a phantom scrollbar on every 'short' page.

The toggle proving it works is its own mini-lesson: two radio buttons and .stage:has(#long:checked) .extra{ display:block } flip the page between one release note and six. Flip back and forth — the footer never detaches from the bottom edge and never overlaps content, which is precisely the contract the old position:fixed footer hack broke (it overlapped; you then padded the body; the padding drifted…).

If what you actually want is a footer that overlays content and pins while scrolling — a cookie bar, a mobile CTA — that's position:sticky; bottom:0 on a LAST child, which is demo 11. Same phrase, different physics; now you have both.

Make it yours

  • Flex or grid: identical results here; grid wins when the header/footer also need columns, flex when you're retrofitting an existing column layout with one margin-top:auto.
  • Multiple pinned rows: grid-template-rows:auto auto 1fr auto handles banner + header + content + footer — the 1fr row is the only magic and there's still just one.
  • Inside a card or panel: the same three lines work at ANY scale — a modal with actions pinned to its bottom edge is this pattern with min-height swapped for height:100%.
  • Overlay variant: to keep the footer visible while long content scrolls (rare, but asked), add position:sticky; bottom:0 to the footer ON TOP of this layout — the grid guarantees position on short pages, sticky handles long ones.

Gotchas — read before shipping

  • 100vh vs 100svh: on iOS Safari, 100vh is taller than the visible viewport with the toolbar expanded — your 'short page' gets a scrollbar and the footer hides below the fold. Ship svh with a vh fallback line above it.
  • height:100svh (instead of min-height) caps the page and long content overflows the grid — the footer floats over it. min- is load-bearing.
  • Setting overflow:auto on .page to 'fix' that overflow quietly makes it a scroll container and any sticky elements inside now pin to IT — fine if intended (this demo's stage works that way), surprising if not.
  • Percentage min-heights need a sized parent: min-height:100% fails silently if an ancestor has auto height — prefer viewport units precisely because they don't depend on ancestry.

Browser support

ChromeSafariFirefoxEdge
111+16.4+121+111+

Grid/flex sticky footers work in every engine since 2017; svh units since Safari 15.4/Chrome 108. The :has() page-length toggle is demo furniture, not part of the pattern — the layout itself has no modern-CSS dependency at all.

Techniques used in this demo

Search CodeFronts

Loading…