Footer

The bottom of a site: a few columns of links, a brand block, and a bar underneath for the copyright and the legal pages. Seven classes, no behaviour, and one decision about link styling that you should make deliberately rather than inherit.

When to use it

On a public site, where the footer is a real navigation surface — the place people go for the legal pages, the contact route, and the parts of the site the main nav does not have room for.

In a logged-in application it is usually dead weight. A dashboard does not need a sitemap at the bottom of every screen, and the vertical space is better spent on the work.

Each column is its own labelled nav; the social row pushes itself to the end
<footer class="footer"> <div class="footer-grid"> <div class="footer-brand"> <strong>Deck</strong> <p>A single-stylesheet CSS framework with no build step.</p> </div> <nav class="footer-col" aria-labelledby="dx-f-prod"> <h2 class="footer-heading" id="dx-f-prod">Product</h2> <a href="#">Components</a> <a href="#">Tokens</a> <a href="#">Changelog</a> </nav> <nav class="footer-col" aria-labelledby="dx-f-res"> <h2 class="footer-heading" id="dx-f-res">Resources</h2> <a href="#">Documentation</a> <a href="#">Examples</a> </nav> </div> <div class="footer-bottom"> <span>© 2026 Deck</span> <a href="#">Privacy</a> <a href="#">Terms</a> <span class="footer-social"> <a href="#" aria-label="Deck on Mastodon"> <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#link"></use></svg> </a> <a href="#" aria-label="Deck on GitHub"> <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#external"></use></svg> </a> </span> </div> </footer>
<footer class="footer">
  <div class="footer-grid">
    <div class="footer-brand">
      <strong>Deck</strong>
      <p>A single-stylesheet CSS framework with no build step.</p>
    </div>
    <nav class="footer-col" aria-labelledby="dx-f-prod">
      <h2 class="footer-heading" id="dx-f-prod">Product</h2>
      <a href="#">Components</a>
      <a href="#">Tokens</a>
      <a href="#">Changelog</a>
    </nav>
    <nav class="footer-col" aria-labelledby="dx-f-res">
      <h2 class="footer-heading" id="dx-f-res">Resources</h2>
      <a href="#">Documentation</a>
      <a href="#">Examples</a>
    </nav>
  </div>
  <div class="footer-bottom">
    <span>© 2026 Deck</span>
    <a href="#">Privacy</a>
    <a href="#">Terms</a>
    <span class="footer-social">
      <a href="#" aria-label="Deck on Mastodon">
        <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#link"></use></svg>
      </a>
      <a href="#" aria-label="Deck on GitHub">
        <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#external"></use></svg>
      </a>
    </span>
  </div>
</footer>

The column grid

.footer-grid is repeat(auto-fit, minmax(min(11rem, 100%), 1fr)) — columns of at least 11rem, as many as fit, collapsing to one on a phone. There is no breakpoint and no column count to set: add a .footer-col and the grid rearranges.

.footer-brand is capped at 26ch, which is a measure rather than a width. A line of about 26 characters is short enough to read as a caption beside the link columns instead of competing with them.

The min(11rem, 100%) is what stops the grid overflowing on a screen narrower than 11rem. It is the standard fix for minmax in an auto-fit track, and it is why this footer does not need a media query.

This is the thing to decide about before you use the component. The footer sets its text to --text-muted, and then sets its links to the same:

.footer { color: var(--text-muted); font-size: var(--text-sm); }
.footer a { color: var(--text-muted); text-decoration: none; }
.footer a:hover { color: var(--brand); text-decoration: underline; }

In a column of links that is fine — everything in the list is a link, so there is nothing to distinguish it from. In a sentence it is a defect. A link inside a paragraph of footer text is the same colour and the same weight as the words around it, with no underline: there is nothing at all to mark it until the pointer reaches it, and a keyboard or touch user gets no hover.

The word "licence" is a link. Nothing on the page says so.
<footer class="footer"> <div class="footer-bottom"> <p>Built with Deck. Read the <a href="#">licence</a> before redistributing.</p> </div> </footer>
<footer class="footer">
  <div class="footer-bottom">
    <p>Built with Deck. Read the <a href="#">licence</a> before redistributing.</p>
  </div>
</footer>

If your footer contains prose, restore the underline for links inside it. It is one rule, and it is the difference between a link people can find and one they cannot:

@layer app.components {
  .footer p a { text-decoration: underline; }
}

Recorded in FINDINGS.md. The column case is a legitimate use of the "links in a list need no underline" exception; the prose case is not covered by it.

The bottom bar

.footer-bottom is a wrapping flex row above a hairline: copyright, the legal links, and anything else short. .footer-social carries margin-inline-start: auto, so wherever you put it in the markup it pushes itself to the far end.

Deck ships no brand marks. There is no Mastodon, GitHub or LinkedIn glyph in deck-icons.svg — 77 icons and not a logo among them, which is deliberate: brand marks have licence terms and change without warning. Supply your own <svg> for a social row; the examples here borrow #link and #external to show the layout.

The social links are 34px squares. That is smaller than Deck's --tap target of 44px and they sit side by side with a .25rem gap — see Accessibility.

Classes

Generated from src/22-nav.css by tools/docs/extract.mjs.

Classes, generated from the stylesheet source
Class Layer What it does Source
.footer deck.components A real component rather than the demo's ad-hoc one. 22-nav.css:672
.footer-bottom deck.components Sets display, flex-wrap, align-items, gap, and 3 more. 22-nav.css:702
.footer-brand deck.components Sets display, flex-direction, gap, max-inline-size. 22-nav.css:684
.footer-col deck.components Sets display, flex-direction, gap. 22-nav.css:685
.footer-grid deck.components Sets display, gap, grid-template-columns, padding-block-end. 22-nav.css:678
.footer-heading deck.components Sets font-size, font-weight, color, margin-block-end. 22-nav.css:686
.footer-social deck.components Sets display, gap, margin-inline-start. 22-nav.css:711

Tokens

Design tokens this component reads
Token Live Declared value
--line light-dark(var(--ink-200), oklch(28% .016 var(--hue-neutral)))
--text-sm .8125rem
--text-xs .75rem
--text light-dark(var(--ink-900), var(--ink-100))
--text-muted light-dark(var(--ink-600), var(--ink-400))
--brand light-dark(var(--brand-600), var(--brand-400))
--surface-hover light-dark(var(--ink-50), var(--ink-900))
--r-sm 7px
--space-8 2rem
--space-10 2.5rem

Accessibility

  • Use the <footer> element. At the top level of the document it is a contentinfo landmark, which is a region screen reader users navigate to directly. .footer is only a class and grants none of that.
  • .footer-heading is not a heading. It is a size, a weight and a colour on whatever element you choose. Use a real <h2> and point the column's aria-labelledby at it, as the first example does — otherwise the columns are three undifferentiated lists of links.
  • Links in prose need an underline. See above. Colour alone is not enough, and here there is not even a colour difference.
  • Social icons need names. An <a> containing only an aria-hidden icon has no accessible name. "Deck on GitHub" rather than "GitHub", so a list of links still makes sense out of context.
  • The social targets are 34px. Below the 44px Deck uses elsewhere, and adjacent, so they are a hard row to hit on a touchscreen. Raise them for coarse pointers — see Overriding it.
  • Footer text is small and muted by default. --text-sm at --text-muted is the lowest-contrast text on a typical page. Check it against your surface, and do not put anything important there that appears nowhere else.
  • Order the markup the way it reads. The grid does not reorder anything, so DOM order is reading order and tab order. Keep it that way.

Right to left

Everything mirrors on its own. The grid flows from the correct edge, the bottom bar's flex row reverses, and .footer-social uses margin-inline-start: auto, so it still ends up at the end of the line rather than pinned to a physical side. Nothing in src/19-logical.css is required.

The social row moves to the left, which is the end of the line
<footer dir="rtl" class="footer"> <div class="footer-grid"> <div class="footer-brand"><strong>ديك</strong><p>إطار عمل CSS بملف واحد.</p></div> <nav class="footer-col" aria-labelledby="dx-f-rtl"> <h2 class="footer-heading" id="dx-f-rtl">المنتج</h2> <a href="#">المكوّنات</a> <a href="#">الرموز</a> </nav> </div> <div class="footer-bottom"> <span>© ٢٠٢٦ ديك</span> <span class="footer-social"> <a href="#" aria-label="ديك على GitHub"> <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#external"></use></svg> </a> </span> </div> </footer>
<footer dir="rtl" class="footer">
  <div class="footer-grid">
    <div class="footer-brand"><strong>ديك</strong><p>إطار عمل CSS بملف واحد.</p></div>
    <nav class="footer-col" aria-labelledby="dx-f-rtl">
      <h2 class="footer-heading" id="dx-f-rtl">المنتج</h2>
      <a href="#">المكوّنات</a>
      <a href="#">الرموز</a>
    </nav>
  </div>
  <div class="footer-bottom">
    <span>© ٢٠٢٦ ديك</span>
    <span class="footer-social">
      <a href="#" aria-label="ديك على GitHub">
        <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#external"></use></svg>
      </a>
    </span>
  </div>
</footer>

Reduced motion

Nothing in the footer is transitioned or animated. The link hover and the social square's background change are both instant, so there is nothing for prefers-reduced-motion to switch off and no guard to add.

Printing

.footer is not in src/99-print.css, so it prints in full. That is more consequential than it sounds: the print stylesheet appends the target address after every link whose href starts with http, so a footer of twenty absolute links prints twenty URLs, each on its own broken line.

@media print {
  /* Usually right: the footer is navigation, and paper cannot follow it */
  .footer-grid, .footer-social { display: none; }

  /* Keep the copyright line, drop the addresses */
  .footer-bottom a::after { content: none; }
}

The copyright and the licence line are usually worth keeping. The sitemap is not.

Overriding it

@layer app.components {
  /* Underline links that sit in sentences */
  .footer p a { text-decoration: underline; }

  /* Full-size social targets on touch devices */
  @media (pointer: coarse) {
    .footer-social a { inline-size: var(--tap); block-size: var(--tap); }
  }

  /* A wider brand block */
  .footer-brand { max-inline-size: 40ch; }

  /* Narrower columns, so more of them fit */
  .footer-grid { grid-template-columns: repeat(auto-fit, minmax(min(8rem, 100%), 1fr)); }
}

When not to use it

  • Not in an application. A dashboard does not need a sitemap under every screen. Put the legal links in a menu off the account control.
  • Not for prose without restoring the underline. See the link section — this is the component's one real defect.
  • Not as the only home for something important. Footer text is the smallest and lowest-contrast on the page, and it is the region people scroll past.
  • Not with .footer-heading on a <div>. The columns need real headings to be navigable.
  • Not for a single row of links. If all you have is a copyright and two legal pages, .footer with just a .footer-bottom is the whole component — the grid is optional and an empty one adds padding for nothing.