CSS Footers: 16 Examples + How to Pin One to the Bottom

Footer bugs mostly show up on short pages. The footer floats halfway up the screen or slides below the fold on a phone, and a few layout rules on the wrapper fix both.

Below are 16 CSS footers you can try and copy, followed by a guide to sticky layouts, mobile viewport units and footer accessibility.

By Bogdan Sandu · Updated October 26, 2026

Multi-column and mega footers

Link-heavy footers for product, shop and nonprofit sites. Every column layout here comes from one grid or one container query, so the same markup goes from four columns on a desktop to a single stack on a phone.

SaaS footer with auto-fit columns

  • repeat(auto-fit, minmax())
  • container query
  • nav aria-label

The four link groups sit in one grid with repeat(auto-fit, minmax(min(100%, 130px), 1fr)), so they drop from four columns to two without a breakpoint. A container query only decides when the brand block moves beside the links. The min(100%, ...) part stops a single column from overflowing on very narrow screens.

View code
.ft-saas {
  container-type: inline-size;
  padding: 56px clamp(20px, 6%, 64px) 0;
  background: #f6f5f1;
  color: #1d1f24;
  font: inherit;
  font-size: 15px;
  line-height: 1.6;
}
:where(.ft-saas) p,
:where(.ft-saas) ul {
  margin: 0;
}
:where(.ft-saas) ul {
  padding: 0;
  list-style: none;
}
.ft-saas__inner {
  display: grid;
  gap: 40px;
}
@container (min-width: 720px) {
  .ft-saas__inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 2.3fr);
    gap: 64px;
  }
  .ft-saas .ft-saas__cols {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}
.ft-saas__brand {
  display: grid;
  align-content: start;
  justify-items: start;
  gap: 14px;
  color: #4d515a;
}
.ft-saas__logo {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  color: #1d1f24;
  font-size: 20px;
  font-weight: 700;
  text-decoration: none;
}
.ft-saas__mark {
  width: 28px;
  height: 28px;
  border-radius: 8px 8px 8px 2px;
  background: #3346d3;
  box-shadow: inset 0 -9px 0 #1f2c99;
}
.ft-saas__status {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  border: 1px solid #d9d7cf;
  border-radius: 100px;
  background: #fff;
  color: #1d1f24;
  font-size: 13.5px;
}
.ft-saas__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #1d8a4e;
}
.ft-saas__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 130px), 1fr));
  gap: 32px 24px;
}
.ft-saas__title {
  margin-bottom: 10px;
  color: #1d1f24;
  font-weight: 700;
}
.ft-saas__cols ul {
  display: grid;
  gap: 2px;
}
.ft-saas__cols li {
  white-space: nowrap;
}
.ft-saas a:not(.ft-saas__logo) {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  color: #4d515a;
  text-decoration: none;
  transition: color .2s ease;
}
.ft-saas a:not(.ft-saas__logo):hover {
  color: #1d1f24;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.ft-saas a:focus-visible {
  outline: 2px solid #3346d3;
  outline-offset: 2px;
  border-radius: 4px;
}
.ft-saas__tag {
  margin-left: 4px;
  padding: 1px 7px;
  border-radius: 100px;
  background: #e3e6fb;
  color: #26338f;
  font-size: 12px;
  font-weight: 700;
}
.ft-saas__legal {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 24px;
  margin-top: 48px;
  padding: 16px 0 20px;
  border-top: 1px solid #dedcd3;
  color: #4d515a;
  font-size: 13.5px;
}
.ft-saas__legal ul {
  display: flex;
  flex-wrap: wrap;
  gap: 0 20px;
}

Mega footer with a newsletter form

  • form + role="status"
  • flex-wrap
  • auto-fit grid

A shop footer in three bands: a signup form, the link columns and a perks and legal row. The input and button share a wrapping flex row, so the button drops under the field on a phone instead of squeezing it. The form never sends anything; the script stops the submit and writes the result into a role="status" line that screen readers announce.

View code
.ft-mega {
  container-type: inline-size;
  padding: 0 clamp(20px, 6%, 64px);
  background: #14261f;
  color: #eef3ef;
  font: inherit;
  font-size: 15px;
  line-height: 1.6;
}
:where(.ft-mega) p,
:where(.ft-mega) ul {
  margin: 0;
}
:where(.ft-mega) ul {
  padding: 0;
  list-style: none;
}
.ft-mega__news {
  display: grid;
  gap: 24px;
  padding: 48px 0 40px;
  border-bottom: 1px solid #2f4a40;
}
@container (min-width: 720px) {
  .ft-mega__news {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: end;
    gap: 48px;
  }
}
.ft-mega__heading {
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(26px, 4vw, 34px);
  font-weight: 600;
  line-height: 1.15;
}
.ft-mega__sub {
  margin-top: 10px;
  color: #b4c5bc;
}
.ft-mega__label {
  display: block;
  margin-bottom: 8px;
  font-size: 14px;
  font-weight: 600;
}
.ft-mega__row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.ft-mega__input {
  flex: 1 1 200px;
  min-width: 0;
  min-height: 48px;
  padding: 0 14px;
  border: 1px solid #5d7d70;
  border-radius: 10px;
  background: #0e1c17;
  color: #fff;
  font: inherit;
}
.ft-mega__input::placeholder {
  color: #93a99e;
}
.ft-mega__btn {
  flex: 0 0 auto;
  min-height: 48px;
  padding: 0 22px;
  border: 0;
  border-radius: 10px;
  background: #e8b04a;
  color: #14261f;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  transition: background-color .2s ease;
}
.ft-mega__btn:hover {
  background: #f2c46b;
}
.ft-mega__input:focus-visible,
.ft-mega__btn:focus-visible,
.ft-mega a:focus-visible {
  outline: 2px solid #e8b04a;
  outline-offset: 2px;
}
.ft-mega__status {
  min-height: 1.6em;
  margin-top: 8px;
  color: #e8b04a;
  font-size: 14px;
}
.ft-mega__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 140px), 1fr));
  gap: 32px 24px;
  padding: 40px 0;
}
.ft-mega__title {
  margin-bottom: 8px;
  font-weight: 700;
}
.ft-mega__cols a {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  color: #b4c5bc;
  text-decoration: none;
  transition: color .2s ease;
}
.ft-mega__cols a:hover {
  color: #fff;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.ft-mega__text {
  color: #b4c5bc;
}
.ft-mega__perks {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding-bottom: 32px;
}
.ft-mega__perks li {
  padding: 8px 14px;
  border: 1px solid #2f4a40;
  border-radius: 100px;
  font-size: 13.5px;
}
.ft-mega__legal {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 4px 24px;
  padding: 18px 0 22px;
  border-top: 1px solid #2f4a40;
  color: #b4c5bc;
  font-size: 13.5px;
}
.ft-mega__legal a {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  margin-right: 14px;
  color: #eef3ef;
}

Columns that turn into an accordion

  • details / summary
  • @container
  • ResizeObserver

Each link group is a details element. In a narrow container the summaries are 52px tap rows that open and close; once the container passes 640px, a container query lays the groups out as four columns and the small script keeps them open and takes the summaries out of the tab order. Without the script, the groups simply stay collapsible at every width.

View code
.ft-acc {
  container-type: inline-size;
  background: #eaf1fb;
  color: #0f2747;
  font: inherit;
  font-size: 15px;
  line-height: 1.6;
}
:where(.ft-acc) p,
:where(.ft-acc) ul {
  margin: 0;
}
.ft-acc__wrap {
  display: grid;
  gap: 28px;
  padding: 44px clamp(20px, 6%, 64px) 24px;
}
.ft-acc__brand {
  max-width: 34ch;
  color: #3c5476;
}
.ft-acc__logo {
  margin-bottom: 6px;
  color: #0f2747;
  font-family: "Fraunces", Georgia, serif;
  font-size: 24px;
  font-weight: 600;
}
.ft-acc__group {
  border-bottom: 1px solid #c4d3e8;
}
.ft-acc__group:first-child {
  border-top: 1px solid #c4d3e8;
}
.ft-acc__group summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 52px;
  font-weight: 700;
  list-style: none;
  cursor: pointer;
}
.ft-acc__group summary::-webkit-details-marker {
  display: none;
}
.ft-acc__group summary::after {
  content: "";
  width: 9px;
  height: 9px;
  margin-right: 6px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
}
.ft-acc__group[open] summary::after {
  transform: translateY(2px) rotate(-135deg);
}
.ft-acc__group summary:focus-visible,
.ft-acc a:focus-visible {
  outline: 2px solid #0f5bd8;
  outline-offset: 2px;
  border-radius: 4px;
}
.ft-acc__group ul {
  display: grid;
  gap: 2px;
  padding: 0 0 14px;
  list-style: none;
}
.ft-acc__group a {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  color: #3c5476;
  text-decoration: none;
  transition: color .2s ease;
}
.ft-acc__group a:hover {
  color: #0f5bd8;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.ft-acc__legal {
  color: #3c5476;
  font-size: 13.5px;
}
.ft-acc__legal a {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  margin-left: 12px;
  color: #0f2747;
}
@media (prefers-reduced-motion: no-preference) {
  .ft-acc__group summary::after {
    transition: transform .25s ease;
  }
}
@container (min-width: 640px) {
  .ft-acc__wrap {
    grid-template-columns: minmax(0, 1fr) minmax(0, 2.4fr);
    gap: 40px 56px;
  }
  .ft-acc__nav {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 24px;
  }
  .ft-acc__group,
  .ft-acc__group:first-child {
    border: 0;
  }
  .ft-acc__group summary {
    min-height: 0;
    margin-bottom: 8px;
    cursor: default;
    pointer-events: none;
  }
  .ft-acc__group summary::after {
    display: none;
  }
  .ft-acc__legal {
    grid-column: 1 / -1;
    padding-top: 18px;
    border-top: 1px solid #c4d3e8;
  }
}

Minimal one-line portfolio footer

  • flex-wrap
  • justify-content
  • gap

For a portfolio, the footer only needs a copyright line, a few links and a location. Three items in a wrapping flex row with space-between give one line on desktop and a tidy stack on a phone. The large next-project link above it does the work a mega footer would do on a bigger site.

View code
.ft-min {
  padding: 0 clamp(20px, 6%, 64px);
  background: #fbfaf7;
  color: #1c1c1a;
  font: inherit;
  line-height: 1.5;
}
:where(.ft-min) p,
:where(.ft-min) ul {
  margin: 0;
}
.ft-min__next {
  display: block;
  padding: 56px 0 48px;
  color: inherit;
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(26px, 4.4vw, 44px);
  font-weight: 500;
  line-height: 1.15;
  text-decoration: none;
}
.ft-min__next span {
  display: inline-block;
}
.ft-min__next:hover {
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 6px;
}
.ft-min__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 6px 28px;
  padding: 18px 0 22px;
  border-top: 1px solid #deddd6;
  color: #575750;
  font-size: 14px;
}
.ft-min__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0 20px;
  padding: 0;
  list-style: none;
}
.ft-min__links a {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  color: #1c1c1a;
  text-decoration: none;
  transition: color .2s ease;
}
.ft-min__links a:hover {
  color: #b23a12;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.ft-min a:focus-visible {
  outline: 2px solid #b23a12;
  outline-offset: 3px;
  border-radius: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .ft-min__next span {
    transition: transform .25s ease;
  }
  .ft-min__next:hover span {
    transform: translateX(8px);
  }
}

Nonprofit footer with landmarks shown

  • footer / nav / address
  • aria-pressed
  • attr()

A charity footer built from the elements screen readers navigate by: footer, a nav labelled Footer, and address for the contact block. Press the button to outline each one. As a direct child of body this footer would be the contentinfo landmark; inside main or article it maps to a generic role, which is why the page footer belongs outside main.

View code
.ft-land {
  padding: 20px clamp(16px, 4%, 40px) 0;
  background: #f4eff8;
  font: inherit;
  font-size: 15px;
  line-height: 1.6;
}
:where(.ft-land) p,
:where(.ft-land) ul {
  margin: 0;
}
.ft-land__toggle {
  min-height: 44px;
  margin-bottom: 20px;
  padding: 0 18px;
  border: 2px solid #2b1838;
  border-radius: 100px;
  background: #fff;
  color: #2b1838;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  transition: background-color .2s ease, color .2s ease;
}
.ft-land__toggle[aria-pressed="true"] {
  background: #2b1838;
  color: #fff;
}
.ft-land__toggle:focus-visible {
  outline: 3px solid #7a3fb0;
  outline-offset: 3px;
}
.ft-land__foot {
  container-type: inline-size;
  padding: 36px clamp(20px, 5%, 48px) 22px;
  border-radius: 22px 22px 0 0;
  background: #2b1838;
  color: #f7f0fb;
}
.ft-land__cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 18px 32px;
  padding-bottom: 28px;
  border-bottom: 1px solid #4a3459;
}
.ft-land__heading {
  max-width: 24ch;
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(22px, 3.4vw, 30px);
  font-weight: 600;
  line-height: 1.2;
}
.ft-land__donate {
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  padding: 0 26px;
  border-radius: 100px;
  background: #ffcf4a;
  color: #2b1838;
  font-weight: 700;
  text-decoration: none;
  transition: background-color .2s ease;
}
.ft-land__donate:hover {
  background: #ffdc7a;
}
.ft-land__grid {
  display: grid;
  gap: 24px 40px;
  padding: 28px 0;
}
@container (min-width: 600px) {
  .ft-land__grid {
    grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  }
}
.ft-land__links {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 140px), 1fr));
  gap: 2px 20px;
  padding: 0;
  list-style: none;
}
.ft-land a:not(.ft-land__donate) {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  color: #f7f0fb;
  text-underline-offset: 3px;
}
.ft-land__links a {
  text-decoration: none;
}
.ft-land__links a:hover {
  text-decoration: underline;
}
.ft-land__addr {
  color: #d9c9e6;
  font-style: normal;
}
.ft-land a:focus-visible {
  outline: 2px solid #ffcf4a;
  outline-offset: 2px;
  border-radius: 4px;
}
.ft-land__legal {
  padding-top: 16px;
  border-top: 1px solid #4a3459;
  color: #d9c9e6;
  font-size: 13.5px;
}
.ft-land [data-ft-label] {
  position: relative;
}
.ft-land.is-on [data-ft-label] {
  outline: 2px dashed #ffcf4a;
  outline-offset: 6px;
}
.ft-land.is-on .ft-land__foot {
  outline-color: #7a3fb0;
  outline-offset: 3px;
}
.ft-land.is-on [data-ft-label]::before {
  content: attr(data-ft-label);
  position: absolute;
  top: -16px;
  right: 0;
  z-index: 1;
  padding: 3px 8px;
  border-radius: 6px;
  background: #ffcf4a;
  color: #2b1838;
  font: 700 12px/1.3 ui-monospace, Consolas, monospace;
}
.ft-land.is-on .ft-land__foot::before {
  top: 8px;
  right: 12px;
  background: #7a3fb0;
  color: #fff;
}

Footers that stay at the bottom

The layout side of footers: a grid frame that holds the footer down on short pages, auto margins inside cards, a curtain reveal, a pinned bar that never hides keyboard focus, and a back-to-top link. Each demo runs in its own box so you can see the technique without scrolling the whole page.

Grid sticky footer on a short page

  • grid-template-rows: auto 1fr auto
  • min-height
  • aria-pressed
Episodes

This episode moved

Season two lives at a new address now. Search the archive or start from the latest show.

The frame is a three-row grid with a minimum height, and the 1fr middle row soaks up the free space, so the footer sits at the bottom even on a nearly empty page. Press the button to add a long article: the frame grows and the footer simply follows the content. On a real page the same three declarations go on body with min-height: 100dvh.

View code
.ft-gs {
  display: grid;
  grid-template-rows: auto 1fr auto;
  min-height: 440px;
  background: #f0f4f8;
  color: #102a43;
  font: inherit;
  font-size: 15px;
  line-height: 1.6;
}
:where(.ft-gs) p {
  margin: 0;
}
.ft-gs__head,
.ft-gs__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 4px 20px;
  padding: 12px clamp(20px, 6%, 48px);
  background: #102a43;
  color: #f0f4f8;
}
.ft-gs__head a,
.ft-gs__foot a {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  color: #bcccdc;
}
.ft-gs__foot a {
  margin-left: 16px;
}
.ft-gs .ft-gs__logo {
  color: #fff;
  font-weight: 700;
  text-decoration: none;
}
.ft-gs__main {
  position: relative;
  display: grid;
  align-content: start;
  justify-items: start;
  gap: 12px;
  margin: 14px;
  padding: 32px clamp(16px, 5%, 40px);
  border: 2px dashed #9fb3c8;
  border-radius: 14px;
}
.ft-gs__main::before {
  content: "1fr row";
  position: absolute;
  top: 8px;
  right: 12px;
  color: #486581;
  font: 600 12px/1 ui-monospace, Consolas, monospace;
}
.ft-gs__title {
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(26px, 4vw, 34px);
  font-weight: 600;
  line-height: 1.15;
}
.ft-gs__main > p:not(.ft-gs__title) {
  max-width: 52ch;
  color: #334e68;
}
.ft-gs__toggle {
  min-height: 44px;
  margin-top: 6px;
  padding: 0 18px;
  border: 2px solid #102a43;
  border-radius: 10px;
  background: #fff;
  color: #102a43;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
.ft-gs__toggle[aria-pressed="true"] {
  background: #102a43;
  color: #fff;
}
.ft-gs a:focus-visible,
.ft-gs__toggle:focus-visible {
  outline: 3px solid #f0b429;
  outline-offset: 2px;
}
.ft-gs__more {
  display: grid;
  gap: 12px;
  max-width: 60ch;
  color: #334e68;
}
.ft-gs__more[hidden] {
  display: none;
}

Card footers aligned with margin-top: auto

  • flex-direction: column
  • margin-top: auto
  • article > footer

Layout

Subgrid for card rows that never drift

Cards in a row rarely share the same amount of text. Subgrid and auto margins are the two clean ways to keep their parts aligned, and they solve slightly different problems.

Ana Ruiz

A footer inside an article is not a page footer: it maps to a generic role and holds the byline for that card. Each card is a flex column and the footer gets margin-top: auto, which absorbs the free space, so the bylines line up even when the excerpts have different lengths.

View code
.ft-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr));
  gap: 18px;
  width: min(100%, 860px);
  font: inherit;
  font-size: 15px;
  line-height: 1.55;
}
:where(.ft-cards) p {
  margin: 0;
}
.ft-cards__item {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 22px;
  border: 1px solid #dfe3ec;
  border-radius: 16px;
  background: #fff;
  color: #4a5160;
}
.ft-cards__tag {
  align-self: flex-start;
  padding: 2px 10px;
  border-radius: 100px;
  background: #eef0f5;
  color: #353b48;
  font-size: 12.5px;
  font-weight: 600;
}
.ft-cards__title {
  font-family: "Fraunces", Georgia, serif;
  font-size: 20px;
  font-weight: 600;
  line-height: 1.25;
}
.ft-cards__title a {
  color: #12151c;
  text-decoration: none;
}
.ft-cards__title a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}
.ft-cards__title a:focus-visible {
  outline: 2px solid #2346c8;
  outline-offset: 3px;
  border-radius: 3px;
}
.ft-cards__foot {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: auto;
  padding-top: 14px;
  border-top: 1px solid #eceef3;
  color: #12151c;
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.35;
}
.ft-cards__foot time {
  color: #5b6271;
  font-weight: 400;
}
.ft-cards__avatar {
  display: grid;
  place-items: center;
  flex: 0 0 36px;
  height: 36px;
  border-radius: 50%;
  background: #ffe1d2;
  color: #8a2c0d;
  font-size: 12.5px;
  font-weight: 700;
}
.ft-cards__avatar--b {
  background: #d9efe3;
  color: #17603b;
}
.ft-cards__avatar--c {
  background: #e1e5fb;
  color: #2b3a9e;
}

Curtain reveal footer

  • position: sticky; bottom: 0
  • z-index
  • :focus-within

Buildings that age well.

Studio Verso designs schools, libraries and housing in timber and reclaimed brick.

  • Harrow Lane Library2025, Bristol
  • Saltmarsh Primary School2024, Norfolk
  • Coopers Yard Housing2023, Leeds
  • Weir Street Studios2022, Glasgow

Scroll inside the frame to reveal the footer.

The footer is sticky at bottom: 0 and sits under the main content, which has a higher z-index and a solid background. As the content scrolls away it uncovers the footer like a curtain. The :focus-within rule lifts the footer above the content whenever a link inside it has keyboard focus, so a tabbed link is never hidden.

View code
.ft-cur {
  height: 480px;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: #1c1712;
  font: inherit;
  font-size: 15px;
  line-height: 1.6;
}
.ft-cur:focus-visible {
  outline: 3px solid #e07a4f;
  outline-offset: -3px;
}
:where(.ft-cur) p,
:where(.ft-cur) ul {
  margin: 0;
}
.ft-cur__main {
  position: relative;
  z-index: 1;
  padding: 48px clamp(20px, 6%, 64px) 56px;
  border-radius: 0 0 28px 28px;
  background: #f3ece2;
  color: #1c1712;
}
.ft-cur__hero {
  max-width: 12ch;
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(40px, 7vw, 76px);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.02em;
}
.ft-cur__lede {
  max-width: 44ch;
  margin-top: 20px;
  color: #4e4338;
  font-size: 17px;
}
.ft-cur__list {
  margin-top: 40px;
  padding: 0;
  list-style: none;
}
.ft-cur__list li {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0 16px;
  padding: 18px 0;
  border-top: 1px solid #d8cbb8;
  font-weight: 600;
}
.ft-cur__list li span + span {
  color: #6b5d4e;
  font-weight: 400;
}
.ft-cur__foot {
  position: sticky;
  bottom: 0;
  z-index: 0;
  display: grid;
  gap: 10px;
  padding: 40px clamp(20px, 6%, 64px) 22px;
  background: #1c1712;
  color: #f3ece2;
}
.ft-cur__foot:focus-within {
  z-index: 2;
}
.ft-cur__big {
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(30px, 5vw, 52px);
  font-weight: 600;
  line-height: 1.05;
}
.ft-cur__mail {
  color: #f0a07c;
  font-size: clamp(18px, 2.4vw, 22px);
  text-underline-offset: 4px;
}
.ft-cur__row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 4px 24px;
  margin-top: 24px;
  padding-top: 14px;
  border-top: 1px solid #4a3f35;
  color: #cbbfae;
  font-size: 13.5px;
}
.ft-cur__row a {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  margin-right: 14px;
  color: #f3ece2;
}
.ft-cur a:focus-visible {
  outline: 2px solid #f0a07c;
  outline-offset: 3px;
}

Pinned bar that never hides focus

  • position: sticky
  • scroll-padding-bottom
  • WCAG 2.4.11
Gabled canal houses beside green water, with two small white boats moored at a wooden jetty

Canal Morning, giclee print

50 x 70 cm, archival matte paper, signed edition of 200

Printed to order on cotton rag paper and shipped flat in a rigid mailer within five working days.

€140Free delivery

Add to bag

A shop page with a purchase bar pinned to the bottom. The scroll container carries scroll-padding-bottom equal to the bar height, so when a keyboard user tabs to a link that would land under the bar, the browser scrolls it clear. That is the CSS technique W3C lists for SC 2.4.11 Focus Not Obscured. On a full page the padding goes on html.

View code
.ft-bar {
  height: 100%;
  overflow-y: auto;
  scroll-padding-bottom: 92px;
  background: #fff;
  color: #1b1d22;
  font: inherit;
  font-size: 15px;
  line-height: 1.55;
}
:where(.ft-bar) p,
:where(.ft-bar) ul {
  margin: 0;
}
.ft-bar__img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}
.ft-bar__body {
  display: grid;
  gap: 8px;
  padding: 20px 20px 28px;
}
.ft-bar__name {
  font-family: "Fraunces", Georgia, serif;
  font-size: 23px;
  font-weight: 600;
  line-height: 1.2;
}
.ft-bar__meta {
  color: #555b66;
  font-size: 14px;
}
.ft-bar__text {
  color: #3c414b;
}
.ft-bar__links {
  margin-top: 8px;
  padding: 0;
  list-style: none;
}
.ft-bar__links a {
  display: flex;
  align-items: center;
  min-height: 48px;
  border-bottom: 1px solid #e6e8ee;
  color: #1b1d22;
  text-decoration: none;
}
.ft-bar__links a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}
.ft-bar__dock {
  position: sticky;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 80px;
  padding: 12px 20px;
  border-top: 1px solid #e6e8ee;
  background: #fff;
}
.ft-bar__price {
  display: block;
  font-size: 20px;
  font-weight: 700;
  line-height: 1.2;
}
.ft-bar__ship {
  color: #1f6b43;
  font-size: 13px;
  font-weight: 600;
}
.ft-bar__buy {
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  padding: 0 24px;
  border-radius: 12px;
  background: #1b1d22;
  color: #fff;
  font-weight: 700;
  text-decoration: none;
  transition: background-color .2s ease;
}
.ft-bar__buy:hover {
  background: #3a3e48;
}
.ft-bar a:focus-visible {
  outline: 3px solid #2457d6;
  outline-offset: 2px;
}

Docs footer with back to top

  • fragment link
  • scroll-behavior
  • aria-pressed

Guides / Webhooks

Configuring webhooks

Webhooks send a POST request to your endpoint each time an event fires in your workspace. Register an endpoint, choose the events, then verify the signature on every delivery.

POST /v2/webhooks
{ "url": "https://example.com/hooks", "events": ["invoice.paid"] }

Deliveries that fail are retried with exponential backoff for up to 24 hours. After that, the event moves to the dead letter list in your dashboard.

Each request carries a timestamp header. Reject any delivery older than five minutes to protect your endpoint against replayed requests.

Scroll to the end of the article, then use Back to top.

A documentation page footer: a helpful-or-not prompt, previous and next links, and a back to top link that is just a fragment link to the top of the article, so it works without script. The script only adds one thing: it moves keyboard focus to the top of the article, so the next Tab starts there. Smooth scrolling only turns on inside prefers-reduced-motion: no-preference.

View code
.ft-docs {
  height: 460px;
  overflow-y: auto;
  background: #fff;
  color: #1f2430;
  font: inherit;
  font-size: 15px;
  line-height: 1.65;
}
@media (prefers-reduced-motion: no-preference) {
  .ft-docs {
    scroll-behavior: smooth;
  }
}
.ft-docs:focus-visible {
  outline: 3px solid #0b63ce;
  outline-offset: -3px;
}
:where(.ft-docs) p {
  margin: 0;
}
.ft-docs__article {
  outline: 0;
  display: grid;
  gap: 14px;
  max-width: 720px;
  padding: 36px clamp(20px, 6%, 56px) 40px;
}
.ft-docs__crumbs {
  color: #5a6272;
  font-size: 13.5px;
}
.ft-docs__title {
  font-size: clamp(26px, 4vw, 34px);
  font-weight: 800;
  line-height: 1.15;
}
.ft-docs__code {
  margin: 0;
  padding: 14px 16px;
  overflow-x: auto;
  border-radius: 10px;
  background: #0f1729;
  color: #e3e8f2;
  font: 13px/1.6 ui-monospace, Consolas, monospace;
}
.ft-docs__foot {
  display: grid;
  gap: 20px;
  padding: 24px clamp(20px, 6%, 56px) 22px;
  border-top: 1px solid #e3e6ec;
  background: #f7f8fa;
}
.ft-docs__feedback {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 10px;
  font-weight: 600;
}
.ft-docs__vote {
  min-width: 56px;
  min-height: 36px;
  padding: 0 14px;
  border: 1px solid #c8cdd8;
  border-radius: 8px;
  background: #fff;
  color: #1f2430;
  font: inherit;
  cursor: pointer;
}
.ft-docs__vote[aria-pressed="true"] {
  border-color: #0b63ce;
  background: #0b63ce;
  color: #fff;
}
.ft-docs__status {
  flex-basis: 100%;
  min-height: 1.4em;
  color: #1d6b3f;
  font-size: 13.5px;
  font-weight: 500;
}
.ft-docs__pager {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
  gap: 12px;
}
.ft-docs__pager a {
  display: grid;
  padding: 12px 16px;
  border: 1px solid #c8cdd8;
  border-radius: 10px;
  background: #fff;
  color: #0b4fa6;
  font-weight: 700;
  text-decoration: none;
  transition: border-color .2s ease;
}
.ft-docs__pager a:last-child {
  text-align: right;
}
.ft-docs__pager a:hover {
  border-color: #0b63ce;
}
.ft-docs__pager span {
  color: #5a6272;
  font-size: 13px;
  font-weight: 500;
}
.ft-docs__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 4px 20px;
  color: #5a6272;
  font-size: 13.5px;
}
.ft-docs__meta a {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  margin-left: 10px;
  color: #0b4fa6;
}
.ft-docs .ft-docs__top {
  margin-left: 0;
  font-weight: 700;
  text-decoration: none;
}
.ft-docs__vote:focus-visible,
.ft-docs a:focus-visible {
  outline: 3px solid #0b63ce;
  outline-offset: 2px;
}

Footers by site type

Agency, magazine, shop, restaurant, app and studio footers. They show what goes into each kind of footer, from a region selector to an address card, and every one is plain HTML and CSS with a script only where the demo needs state.

Agency footer with a big wordmark

  • container query units
  • cqi font-size
  • Fraunces

The studio name is set in cqi units, a share of the footer width, so it spans edge to edge at any size without a single media query. A vw value comes first as the fallback for browsers without container units. The small columns above it hold the address, the enquiries line and text-only social links.

View code
.ft-word {
  container-type: inline-size;
  overflow: hidden;
  padding: 44px clamp(20px, 5%, 56px) 0;
  background: #f1ece1;
  color: #171512;
  font: inherit;
  font-size: 15px;
  line-height: 1.6;
}
:where(.ft-word) p {
  margin: 0;
}
.ft-word__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr));
  gap: 24px;
  color: #4d473e;
}
.ft-word__title {
  margin-bottom: 4px;
  color: #171512;
  font-weight: 700;
}
.ft-word a {
  display: inline-block;
  padding: 2px 0;
  color: #171512;
  text-underline-offset: 3px;
}
.ft-word a:hover {
  color: #a3361a;
}
.ft-word a:focus-visible {
  outline: 2px solid #a3361a;
  outline-offset: 2px;
}
.ft-word__mark {
  margin: 24px 0 0 -0.04em;
  font-family: "Fraunces", Georgia, serif;
  font-size: 31vw;
  font-size: 35cqi;
  font-weight: 700;
  line-height: .9;
  letter-spacing: -0.045em;
  white-space: nowrap;
}
.ft-word__legal {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 2px 20px;
  padding: 14px 0 18px;
  border-top: 2px solid #171512;
  color: #4d473e;
  font-size: 13.5px;
}

Dark magazine footer with text social links

  • text links, no icons
  • transform: scaleX
  • auto-fit grid

Social links written as words need no icon font, no aria-label and no brand artwork, and they are easy to read and tap. The hover underline is a pseudo-element scaled on the x axis, and it only animates for users who have not asked for reduced motion. Every text color clears 4.5:1 on the near-black background.

View code
.ft-mag {
  padding: 52px clamp(20px, 6%, 64px) 24px;
  background: #101114;
  color: #f1eee7;
  font: inherit;
  font-size: 15px;
  line-height: 1.6;
}
:where(.ft-mag) p,
:where(.ft-mag) ul {
  margin: 0;
}
.ft-mag__lead {
  max-width: 30ch;
  color: #b9b4aa;
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(24px, 3.6vw, 34px);
  font-weight: 400;
  line-height: 1.25;
}
.ft-mag__lead span {
  color: #f1eee7;
  font-weight: 700;
}
.ft-mag__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr));
  gap: 28px 24px;
  margin-top: 40px;
  padding-top: 28px;
  border-top: 1px solid #2c2d33;
}
.ft-mag__title {
  margin-bottom: 8px;
  color: #b9b4aa;
  font-size: 13.5px;
}
:where(.ft-mag) ul {
  display: grid;
  gap: 2px;
  padding: 0;
  list-style: none;
}
.ft-mag a {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 34px;
  color: #f1eee7;
  font-size: 17px;
  text-decoration: none;
}
.ft-mag a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 4px;
  height: 1px;
  background: #e9c46a;
  transform: scaleX(0);
  transform-origin: left;
}
.ft-mag a:hover::after,
.ft-mag a:focus-visible::after {
  transform: scaleX(1);
}
.ft-mag a span {
  color: #e9c46a;
  font-size: 14px;
}
.ft-mag a:focus-visible {
  outline: 2px solid #e9c46a;
  outline-offset: 3px;
  border-radius: 2px;
}
.ft-mag__colophon {
  margin-top: 40px;
  padding-top: 16px;
  border-top: 1px solid #2c2d33;
  color: #a5a097;
  font-size: 13.5px;
}
@media (prefers-reduced-motion: no-preference) {
  .ft-mag a::after {
    transition: transform .3s ease;
  }
}

Restaurant footer with a map card

  • address
  • description list
  • container query

For a local business the footer is where people look for the address, the hours and a phone number, so those come first. The map is a lightweight SVG image instead of an embedded map frame, and the Directions link hands off to a real map app. The hours sit in a description list so each day stays paired with its times.

View code
.ft-map {
  container-type: inline-size;
  padding: 36px clamp(16px, 5%, 48px) 0;
  background: #fbf4ec;
  color: #2a1a12;
  font: inherit;
  font-size: 15px;
  line-height: 1.55;
}
:where(.ft-map) p {
  margin: 0;
}
.ft-map__grid {
  display: grid;
  gap: 24px;
}
@container (min-width: 680px) {
  .ft-map__grid {
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    align-items: center;
    gap: 40px;
  }
}
.ft-map__img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid #e6d6c2;
  border-radius: 18px;
}
.ft-map__info {
  display: grid;
  gap: 14px;
}
.ft-map__name {
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(28px, 4vw, 36px);
  font-style: italic;
  font-weight: 600;
  line-height: 1.1;
}
.ft-map__addr {
  color: #5a4436;
  font-style: normal;
}
.ft-map__addr a {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  color: #a8321d;
  font-weight: 700;
}
.ft-map__hours {
  display: grid;
  margin: 0;
  border-top: 1px solid #e6d6c2;
}
.ft-map__hours div {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 0;
  border-bottom: 1px solid #e6d6c2;
}
.ft-map__hours dt {
  font-weight: 700;
}
.ft-map__hours dd {
  margin: 0;
  color: #5a4436;
}
.ft-map__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.ft-map__primary,
.ft-map__secondary {
  display: inline-flex;
  align-items: center;
  min-height: 46px;
  padding: 0 20px;
  border: 2px solid #a8321d;
  border-radius: 100px;
  font-weight: 700;
  text-decoration: none;
  transition: background-color .2s ease, color .2s ease;
}
.ft-map__primary {
  background: #a8321d;
  color: #fff;
}
.ft-map__primary:hover {
  background: #8a2714;
}
.ft-map__secondary {
  color: #a8321d;
}
.ft-map__secondary:hover {
  background: #a8321d;
  color: #fff;
}
.ft-map a:focus-visible {
  outline: 3px solid #2a1a12;
  outline-offset: 2px;
}
.ft-map__legal {
  margin-top: 32px;
  padding: 16px 0 20px;
  border-top: 1px solid #e6d6c2;
  color: #5a4436;
  font-size: 13.5px;
}
.ft-map__legal a {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  margin-left: 12px;
  color: #2a1a12;
}

App download footer with text buttons

  • text buttons, no badges
  • CSS phone mockup
  • @container

Store badges are trademarked artwork with their own usage rules, so these download buttons are plain text links that say which platform they are for. The phone beside them is drawn in CSS and hidden from assistive technology, and a container query drops it when the footer gets too narrow to hold both.

View code
.ft-app {
  container-type: inline-size;
  padding: 0 clamp(20px, 6%, 64px);
  background: #11140f;
  color: #f4f7ee;
  font: inherit;
  font-size: 15px;
  line-height: 1.55;
}
:where(.ft-app) p {
  margin: 0;
}
.ft-app__grid {
  display: grid;
  align-items: center;
  gap: 32px;
  padding: 48px 0 36px;
}
@container (min-width: 620px) {
  .ft-app__grid {
    grid-template-columns: minmax(0, 1fr) 210px;
    padding-bottom: 0;
  }
}
.ft-app__heading {
  max-width: 16ch;
  font-size: clamp(30px, 5vw, 46px);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.02em;
}
.ft-app__text {
  max-width: 40ch;
  margin-top: 14px;
  color: #c3c9b8;
}
.ft-app__get {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 24px;
}
.ft-app__get a {
  display: grid;
  min-width: 150px;
  min-height: 54px;
  padding: 8px 18px;
  border-radius: 12px;
  background: #d6f55a;
  color: #11140f;
  font-size: 17px;
  font-weight: 800;
  line-height: 1.15;
  text-decoration: none;
  transition: background-color .2s ease;
}
.ft-app__get a + a {
  background: transparent;
  box-shadow: inset 0 0 0 2px #d6f55a;
  color: #f4f7ee;
}
.ft-app__get a:hover {
  background: #e4fa8c;
  color: #11140f;
}
.ft-app__get span {
  font-size: 12.5px;
  font-weight: 600;
}
.ft-app a:focus-visible {
  outline: 3px solid #fff;
  outline-offset: 3px;
}
.ft-app__rating {
  margin-top: 16px;
  color: #c3c9b8;
  font-size: 14px;
}
.ft-app__rating strong {
  color: #f4f7ee;
}
.ft-app__phone {
  display: none;
}
@container (min-width: 620px) {
  .ft-app__phone {
    display: block;
    align-self: end;
    height: 300px;
    padding: 12px 12px 0;
    border: 2px solid #3a4133;
    border-bottom: 0;
    border-radius: 34px 34px 0 0;
    background: #1b2017;
  }
}
.ft-app__screen {
  height: 100%;
  padding: 24px 18px;
  border-radius: 24px 24px 0 0;
  background: #f4f7ee;
  color: #11140f;
}
.ft-app__day {
  font-size: 13px;
  font-weight: 700;
}
.ft-app__km {
  margin-top: 10px;
  font-size: 52px;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.03em;
}
.ft-app__km small {
  font-size: 18px;
}
.ft-app__pace {
  color: #4b5240;
  font-size: 13px;
}
.ft-app__bars {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  height: 90px;
  margin-top: 22px;
}
.ft-app__bars i {
  flex: 1;
  border-radius: 4px 4px 0 0;
  background: #11140f;
}
.ft-app__bars i:nth-child(1) { height: 45%; }
.ft-app__bars i:nth-child(2) { height: 70%; }
.ft-app__bars i:nth-child(3) { height: 100%; background: #7ea21b; }
.ft-app__bars i:nth-child(4) { height: 82%; }
.ft-app__bars i:nth-child(5) { height: 64%; }
.ft-app__bars i:nth-child(6) { height: 38%; }
.ft-app__legal {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 4px 20px;
  padding: 16px 0 20px;
  border-top: 1px solid #2d3327;
  color: #c3c9b8;
  font-size: 13.5px;
}
.ft-app__legal a {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  margin-right: 14px;
  color: #f4f7ee;
}

Contact CTA footer

  • mailto link
  • clamp() with cqi
  • overflow-wrap

An agency footer that is mostly one call to action: a question, the email address as a big link, and a booking button. The email is sized in cqi units of the footer width so it fits on one line, and overflow-wrap: anywhere is the safety net for longer addresses. Dark text on the orange background measures about 5.7:1.

View code
.ft-cta {
  container-type: inline-size;
  padding: 48px clamp(20px, 6%, 64px) 0;
  background: #f25c2a;
  color: #14110f;
  font: inherit;
  font-size: 15px;
  line-height: 1.55;
}
:where(.ft-cta) p {
  margin: 0;
}
.ft-cta__avail {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 14px;
  border: 1.5px solid #14110f;
  border-radius: 100px;
  font-size: 13.5px;
  font-weight: 600;
}
.ft-cta__avail span {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #14110f;
}
.ft-cta__ask {
  margin-top: 28px;
  font-size: clamp(20px, 3vw, 26px);
  font-weight: 600;
}
.ft-cta__mail {
  display: inline-block;
  margin-top: 6px;
  color: #14110f;
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(22px, 6.4cqi, 72px);
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: -0.02em;
  text-decoration-thickness: 3px;
  text-underline-offset: 8px;
  overflow-wrap: anywhere;
}
.ft-cta__mail:hover {
  text-decoration-thickness: 6px;
}
.ft-cta__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 32px;
}
.ft-cta__book,
.ft-cta__work {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 50px;
  padding: 0 24px;
  border: 2px solid #14110f;
  border-radius: 100px;
  font-weight: 700;
  text-decoration: none;
  transition: background-color .2s ease, color .2s ease;
}
.ft-cta__book {
  background: #14110f;
  color: #fff;
}
.ft-cta__work {
  color: #14110f;
}
.ft-cta__work:hover {
  background: #14110f;
  color: #fff;
}
.ft-cta__book span {
  display: inline-block;
}
.ft-cta a:focus-visible {
  outline: 3px solid #fff;
  outline-offset: 3px;
}
.ft-cta__base {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 4px 20px;
  margin-top: 48px;
  padding: 16px 0 20px;
  border-top: 1.5px solid #14110f;
  font-size: 13.5px;
}
.ft-cta__base a {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  margin-right: 14px;
  color: #14110f;
}
@media (prefers-reduced-motion: no-preference) {
  .ft-cta__book span {
    transition: transform .25s ease;
  }
  .ft-cta__book:hover span {
    transform: translateX(5px);
  }
}

How do you build a footer with HTML and CSS?

Put a footer element after the main content, then give it a background-color, padding and text-align in CSS.

  1. Add the footer element as a direct child of body, after main.
  2. Fill it with a nav of links and a copyright line, plus contact details if the site needs them (an address element suits author or contact info).
  3. Set box-sizing to border-box on every element and reset the body margin to 0, so padding adds no stray width.
  4. Style the footer with background-color, color, padding and text-align.
  5. Give the links a hover state, a :focus-visible outline and enough spacing to tap.
<body>
  <main>...</main>
  <footer>
    <nav aria-label="Footer">
      <a href="/privacy">Privacy</a>
      <a href="/contact">Contact</a>
    </nav>
    <p>Copyright 2026 Example Studio</p>
  </footer>
</body>
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; }
footer { background-color: #1f2933; color: #f5f7fa; padding: 24px 16px; text-align: center; }
footer a { color: inherit; }
footer a:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }

Nothing in that skeleton goes past standard HTML, and the stylesheet is short.

MDN’s footer reference lists the element as available across browsers since July 2015, so no fallback markup is needed.

Footer markup that gets the contentinfo role

A footer that isn’t nested inside article, aside, main, nav or section maps to the contentinfo landmark, and MDN advises making it an immediate descendant of body. Each page gets one top-level contentinfo landmark, per MDN’s ARIA contentinfo page. Footer and header elements aren’t allowed as descendants of a footer.

Before Safari 13, VoiceOver did not expose the role from the element alone. Add role=”contentinfo” only if that legacy support is still on the requirements list.

Footer inside an article or section

A footer nested in article, aside, main, nav or section is not a page footer.

MDN lists its implicit role as generic in those cases, so it can carry a byline, date or related links for that block without creating a second landmark. Screen reader users don’t get a landmark shortcut to it, which is fine for byline metadata.

How do you keep a footer at the bottom of a short page?

Turn the page wrapper into a column flex container or a 3-row grid with a minimum height equal to the viewport, and let the main area take the leftover space. The footer then sits at the viewport bottom on short pages and below the content on long ones.

TechniqueKey declarationsFooter height must be knownBest fit
Flexbox, flex-growColumn flex wrapper, flex-grow: 1 on mainNoWrapper is already a flex column
Flexbox, auto marginmargin-top: auto on the footerNoOne footer rule, no main selector
Grid, 3 rowsgrid-template-rows: auto 1fr autoNoHeader, main and footer page frame
Negative margin (legacy)margin-bottom equal to footer heightYesVery old browsers only

Flexbox method

Make the body the wrapper and stack its children in a column. Main then grows to fill whatever space is left.

body { min-height: 100vh; display: flex; flex-direction: column; }
main { flex-grow: 1; }
footer { flex-shrink: 0; }

MDN’s recipe sets flex-shrink: 0 on the header and footer so they never compress when content fills the main area. An auto top margin on the footer gives the same result without touching main, because auto margins absorb free space in a flex container.

On phones, 100vh has a toolbar problem, which the viewport units section below deals with.

Grid method

MDN’s Sticky footers recipe uses auto, 1fr and auto for the rows.

body { min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; }

Grid auto-placement drops the header into the first auto track, main into the 1fr track and the footer into the last auto track. The 1fr track soaks up the free space.

An extra direct child (a skip link, a cookie banner container) shifts every item one track down, and the 1fr row ends up holding the wrong element.

Which method to pick

For a full page frame I’d go with grid, as long as header, main and footer are the only children of the wrapper. If the wrapper is already a flex column, or script adds and removes children, flexbox is the safer pick. And when one footer rule is all the stylesheet should change, margin-top: auto does it.

Legacy methods to skip

The negative margin technique hard-codes the footer height in the stylesheet. When footer text wraps on a narrow screen, the footer outgrows that number and overlaps the content.

Absolute positioning with body padding has the same weakness. Flexbox and grid carry no such dependency.

Fixed, absolute and sticky footers: when each one fits

Keep the footer in normal flow by default. Reach for position: fixed only when it must stay on screen during scrolling, such as a consent bar or a persistent action bar.

Fixed footer

A fixed footer stays visible at any scroll position and positions against the viewport, so no wrapper height is needed.

The downsides are practical. It leaves normal flow, so it covers the last content block until the body gets padding-bottom equal to the footer height. Any ancestor with transform, perspective or filter becomes the containing block instead of the viewport (MDN). It can also obscure content when users enlarge text, which MDN flags as an accessibility risk for fixed and absolute elements.

MDN also suggests will-change: transform on fixed or sticky elements that cause repaint jank while scrolling.

Absolute footer

An absolute footer anchors to its closest positioned ancestor. Without one, it anchors to the initial containing block, which has the dimensions of the viewport and sits at the top of the document.

So bottom: 0 places the footer at the bottom of the first screen on a long page, behind the content. That is one cause of a footer floating mid-page.

It works with a known footer height and a positioned wrapper, and needs no extra markup. The catch is padding-bottom on the wrapper equal to the footer height, and the whole setup breaks when footer text wraps.

Sticky footer with position: sticky

MDN’s position reference describes a sticky element as relatively positioned until its containing block crosses a set threshold.

That makes position: sticky with bottom: 0 a way to pin a footer to the viewport bottom while its containing block scrolls past. It does not push a footer down on a short page.

At least one of top, right, bottom or left has to be non-auto, otherwise the element behaves as relative. It also sticks to the nearest ancestor with overflow set to hidden, scroll or auto, even when that ancestor never scrolls. On short pages, pair it with the flex or grid wrapper from the previous section.

Why 100vh breaks footer layouts on mobile, and what replaces it

On phones, 100vh matches the page with the browser toolbars retracted, so a wrapper with min-height: 100vh is taller than the visible area on load. The footer lands below the fold until the user scrolls.

The replacement is 100dvh, or 100svh when stability matters more than filling every pixel. Both come from web.dev’s viewport units article (Chrome team, last updated November 29, 2022).

Mobile browsers resize the visible viewport as the address bar and tab bar expand and retract, while vh stays fixed. svh covers the small viewport, with toolbars expanded. lvh covers the large one, toolbars retracted, and 100vh behaves like this. dvh equals svh while toolbars are expanded and lvh once they retract.

body { min-height: 100vh; min-height: 100dvh; }

Browsers that cannot parse dvh drop the second declaration and keep the first.

For a footer wrapper, that pair is the default. Switch to 100svh if the footer visibly jumps while toolbars animate, since the small viewport values stay fixed unless the viewport itself resizes.

Support, per web.dev in November 2022, starts at version 108 in Chrome and Edge, 101 in Firefox and 15.4 in Safari. Dynamic viewport values also update in a throttled way, below 60fps. Those versions date from 2022, and the units have been Baseline Widely available since June 2025, so the 100vh fallback now only covers very old browsers.

MDN’s own recipe takes a different route: html and body at height 100%, with a wrapper at min-height: 100%, which avoids viewport units entirely. Its note offers min-height: 100vh on body as the alternative, and that alternative inherits the mobile overshoot.

Some third-party guides claim browsers disagree on what vh measures. web.dev, the Chrome team’s own article, treats vh as a fixed size that overshoots while toolbars are expanded.

The on-screen keyboard does not resize any of these units unless Chrome’s opt-in behavior is switched on. A footer sized with dvh stays put when a form field opens the keyboard.

For a wider look at vw, vmin and the newer units, see this guide to CSS viewport units.

How do you build a multi-column footer that stacks on small screens?

Give the columns a grid with repeat(auto-fit, minmax(…)) tracks and a gap, and they wrap to fewer columns as the width shrinks. Add a media query only when the design needs an exact column count at a given width.

:root { --footer-gap: 32px; }

.footer-columns {
  display: grid;
  gap: var(--footer-gap);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
}

Custom properties keep the gap, colors and spacing in one place, so the columns and the icon strip never drift apart.

Grid columns

Grid keeps column edges aligned across rows, which suits link groups under headings.

With auto-fit, empty tracks collapse, so 3 columns in a 4-track row stretch to fill it. With auto-fill they stay reserved, and the columns keep the width of a single track instead of stretching.

For a fixed count, write repeat(4, minmax(0, 1fr)). Plain 1fr cannot shrink below its content, so a long email address or URL widens its column and pushes the page sideways.

Flexbox columns

Flex wrap works, but every wrapped row sizes itself.

.footer-columns { display: flex; flex-wrap: wrap; gap: var(--footer-gap); }
.footer-columns > * { flex: 1 1 200px; }

A lone column on the last row stretches across the full width, so its edges stop lining up with the rows above.

Flexbox earns its place in the one-row parts of a footer, like the social icon strip and the legal link line.

Stacking rules

Write the stacked layout as the default, then widen it with min-width queries, as mobile-first design prescribes.

.footer-columns { grid-template-columns: 1fr; }

@media (min-width: 640px) {
  .footer-columns { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 960px) {
  .footer-columns { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

Those two media queries are placeholders. Resize the window until link text starts wrapping, then set the breakpoint there instead of at a device width.

Tab order follows the DOM, so the column a keyboard user should reach first has to come first in the markup.

Footer accessibility: contrast, focus, landmark labels and target size

A footer can fail WCAG 2.2 in several unrelated ways, so each of these gets its own check.

Text contrast

W3C’s Understanding document for SC 1.4.3 sets the Level AA minimum at 4.5:1 for normal text and 3:1 for large text, meaning 18 point or 14 point bold.

Hover and keyboard focus states count, because the criterion covers text revealed on hover or focus. Brand guidelines are no exemption, since only logos and logotype text are exempt. Set color and background-color together as well, because W3C lists specifying one without the other as failure technique F24.

Test each pair in a checker, and read this primer on color contrast if the ratio math is new.

Keyboard focus behind a pinned footer

A pinned footer can hide the very link that has keyboard focus.

WCAG 2.2 SC 2.4.11, Focus Not Obscured (Minimum), is Level AA and names sticky footers as a typical offender. W3C documents CSS scroll-padding as the sufficient technique (C43) and lists a footer that completely hides a focused element as failure F110.

:root { --footer-height: 72px; }

footer { position: fixed; inset: auto 0 0; height: var(--footer-height); }
body { padding-bottom: var(--footer-height); }
html { scroll-padding-bottom: var(--footer-height); }

Body padding clears the last block of content, while scroll-padding keeps tabbed elements out from under the footer at every scroll position.

The footer height lives in a variable now, so a taller footer on mobile needs the variable changed in its media query too.

Landmark labels

When a document has more than one contentinfo or footer, MDN says to give each an aria-label. A single page footer is better left unlabeled, since MDN notes that role=”contentinfo” with aria-label=”Footer” may be announced redundantly as “contentinfo footer”.

The label earns its keep on repeated landmarks. A short ARIA label on the footer nav tells it apart from the header nav in a screen reader’s landmark list.

Target size and icon-only links

WCAG 2.2 SC 2.5.8 (Level AA) asks for a target area of at least 24 by 24 CSS pixels, or enough spacing that a 24 px circle centered on the target touches no other target.

Links inside a sentence are exempt. A list of footer links is not, and neither is a row of icons.

footer a { display: inline-block; min-width: 24px; min-height: 24px; padding: 4px; }
<a href="https://example.com/linkedin" aria-label="Example Studio on LinkedIn">
<i class="fa-brands fa-linkedin" aria-hidden="true"></i>
</a>

Deque’s axe-core target-size rule measures the largest unobscured area of each target and checks the 24 px spacing circle, so it catches icons that sit too close together.

Font Awesome’s documentation disagrees with its own older versions on icon fonts. As of October 2026, the current docs say web font icons are ignored by assistive technology by default and that SVG icons injected by a Kit get aria-hidden automatically, while the version 4 docs warn that screen readers can read CSS-generated icon content and tell authors to add aria-hidden themselves.

Putting aria-hidden on the icon and the name on the link works under both versions.

What Lighthouse reports

As of October 2026, Chrome for Developers lists “Links have a discernible name” and “Background and foreground colors have a sufficient contrast ratio” at a weight of 7 each in the accessibility score. Both are pass or fail per page, so if some links have names and others do not, the audit scores 0. One unnamed icon link in the footer is enough.

Tab through the footer once by hand as well. Overlap with a pinned footer shows up only at particular scroll positions.

Why does a footer float, cover content or overflow on mobile?

Match the symptom, not the property, because each failure comes from a different layer of the CSS.

SymptomLikely causeFix
Footer ends mid-page on short contentWrapper min-height: 100% inside an html or body of auto heightSet html and body to height: 100%, or put min-height on the body
Last block of content hidden under the footerFixed footer left normal flowBody padding-bottom plus html scroll-padding-bottom
Horizontal scrollbar, footer wider than the pagewidth: 100vw ignores classic scrollbarswidth: 100% on the footer
Footer cut off at the bottom on phone load100vh taller than the visible areamin-height: 100dvh with a 100vh fallback
Footer jumps while scrolling on a phonedvh updates as toolbars resizemin-height: 100svh

A percentage min-height only works when the parent has a definite height. That is why MDN’s sticky footer recipe sets height: 100% on html and body before it sets min-height: 100% on the wrapper.

The 100vw row comes from web.dev. Viewport units assume scrollbars do not exist, so a 100vw element is slightly too wide wherever classic scrollbars take space.

Inspecting the wrapper in DevTools

In Chrome DevTools, a grid or flex badge appears next to any element with display: grid or display: flex. Toggling it draws an overlay with columns, rows, numbers and gaps for grid, or child positions for flex.

Firefox Developer Tools work a little differently. Clicking the grid icon beside display: grid in the Rules view toggles an overlay, and the Layout panel adds line numbers, area names, infinite line extension and a mini grid view.

Check the footer with near-empty content and with content taller than the viewport, then again at a narrow phone width.

Desktop Chrome has no dynamic toolbars, and web.dev notes that the large, small and dynamic viewports are identical there. The 100vh overshoot cannot show up in desktop emulation, so confirm it on a real phone.

Common Questions About CSS Footers

What should a website footer contain?

MDN’s footer reference says a footer typically holds author information, copyright data or links to related documents. In practice that means a copyright line, privacy and terms links, contact details and a short secondary nav.

Does Bootstrap include a sticky footer class?

Not as a single class. Its sticky-bottom and fixed-bottom helpers pin an element to the viewport, which is a different pattern. Its flex utilities build a sticky footer, though, with d-flex, flex-column and min-vh-100 on the wrapper and mt-auto on the footer.

Ready-made Bootstrap footer layouts follow the same pattern.

Can Tailwind CSS keep a footer at the bottom of a short page?

Yes. Put min-h-screen, flex and flex-col on the wrapper, then add mt-auto to the footer or grow to main.

Finished Tailwind footer layouts use this structure. Note that min-h-screen maps to 100vh, so on phones swap it for the min-h-dvh utility, which maps to min-height: 100dvh.

Should a footer have a fixed height?

No, leave the height on auto and let padding size the footer. A hard-coded height clips or overlaps content when link text wraps on a narrow screen.

The exception is a fixed-position footer, where body padding needs a known height. Store that value in one custom property.

How do you center footer content in CSS?

Text-align: center handles inline text and links. For blocks, give the inner container display: flex with justify-content: center, or set a max-width and margin-inline: auto.

How do you style footer links with a hover state and a top border?

Add border-top to the footer rule, set the links to color: inherit, and write the hover and :focus-visible states in one selector.

Mouse and keyboard users then get the same cue.

How do you update the copyright year automatically?

Set the text from new Date().getFullYear() in a short script. Better still, render the year at build or server time, because text set by script is missing when JavaScript is off.

Testing CSS Footers Before the Page Ships

Start with the layout, using a short page and then a page taller than the viewport. After that, open it on a real phone and watch the toolbars expand and retract. Tab through the footer last.

The keyboard check goes last because the footer height variable feeds both the body padding and the scroll-padding. Those values must be final before a tab-through proves anything.

The cost is rework. A late change, such as a taller footer on mobile, sends the pass back to the first step.

This order was verified in October 2026 against WCAG 2.2. A new WCAG version that changes the AA focus or target size criteria would change the final step.

Once the footer passes, the same checks apply to the matching CSS header at the top of the page.