GODRICH

웹사이트 푸터 디자인 패턴 9종, 복사해서 바로 쓰는

웹사이트 푸터 디자인은 페이지 맨 아래에서 방문자를 붙잡거나 내보내는 마지막 화면입니다. 여기 모은 아홉 가지는 링크 나열로 끝나지 않는 푸터를 HTML 과 CSS 만으로 짜 둔 것이라, 그대로 복사해 붙이면 바로 움직입니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 유행 순서가 아니라 방문자가 푸터에서 겪는 순서로 놓았습니다. 먼저 링크가 가장 많은 다단 사이트맵(01)으로 길을 펼치고, 그 옆에서 이메일을 받아 관계를 잇고(02), 반대로 아무것도 묻지 않고 한 줄로 닫는 쪽(03)을 보여 드립니다. 손이 화면 아래에 머무는 모바일에서는 푸터가 아예 조작바가 되고(04), 계정으로 나가는 길은 큼직한 타일(05)과 계단식 링크 트리(06)로 갈립니다. 마지막 셋은 푸터의 바탕 자체를 다룹니다 — 오시는 길을 지도 카드로 얹고(07), 본문과 푸터 사이의 경계를 물결로 잇고(08), 밝은 본문을 어두운 잉크 바닥으로 눌러 닫습니다(09). 아홉 개가 잉크 #1b1a20 · 크림 #fbf6ee · 아쿠아 #6ee7d7 라는 같은 색 값을 공유하므로 둘셋을 섞어 써도 색이 어긋나지 않습니다.

01다단 사이트맵 푸터

넓은 화면에서는 링크 묶음 세 단이 나란히 서고, 폭이 좁아지면 각 단이 열리고 닫히는 아코디언이 됩니다. 여닫히는 것은 높이가 아니라 그리드 트랙입니다 — height: auto 는 중간값이 계산되지 않지만 0fr1fr 사이는 계산되기 때문입니다. 링크가 스무 개를 넘는 서비스 사이트 하단에 씁니다.

grid-template-rowsaria-expanded:focus-visible
.fsm__panel {
  display: grid;
  grid-template-rows: 1fr;          /* 열린 상태 */
  overflow: hidden;
  transition: grid-template-rows 300ms cubic-bezier(.2, .8, .2, 1);
}
.fsm__panel > * { min-height: 0; }

/* 접힌 단 — 버튼의 aria-expanded 가 상태의 유일한 출처다 */
.fsm__col:has(.fsm__btn[aria-expanded="false"]) .fsm__panel {
  grid-template-rows: 0fr;
}
.fsm__btn[aria-expanded="false"] .fsm__caret { transform: rotate(-90deg); }

02뉴스레터 통합 푸터

이메일을 넣고 보내면 버튼이 눌리고, 로딩 표시가 돌고, 완료 도장이 튀어오르는 세 장면이 같은 자리에서 이어집니다. 아이콘 석 장과 안내 문구 넉 장을 각각 같은 그리드 칸에 포개 두었기 때문에 상태가 바뀌어도 버튼 폭과 줄 높이가 흔들리지 않습니다. 구독을 받는 푸터에 씁니다.

aria-livesteps(1, end)translateY
/* 아이콘 석 장을 같은 칸에 포갠다 — 상태가 바뀌어도 버튼 폭이 안 흔들린다 */
.fn__icons { display: grid; place-items: center; width: 18px; height: 18px; }
.fn__ic { grid-area: 1 / 1; opacity: 0; }
.fn.is-idle .fn__ic--plane { opacity: 1; }
.fn.is-sending .fn__ic--load { opacity: 1; }
.fn.is-done .fn__ic--done { opacity: 1; }

/* 보내는 동안 차오르는 진행 게이지 */
.fn__bar {
  height: 2px;
  border-radius: 999px;
  background: #6ee7d7;
  transform: scaleX(0);
  transform-origin: left center;
}
.fn.is-sending .fn__bar { transform: scaleX(.6); }
.fn.is-done .fn__bar { transform: scaleX(1); }

03미니멀 원라인 푸터

로고와 저작권, 동그란 소셜 버튼 세 개가 한 줄로 끝납니다. 버튼 위를 지나면 점이 6px 떠오르면서 계정마다 정해 둔 색으로 차는데, 그 색을 --on 이라는 변수 하나로 받아 두어 버튼마다 규칙을 새로 쓰지 않습니다. 보여 줄 것이 적은 포트폴리오 하단에 씁니다.

translateYscaleX:focus-visible
.fo__dot {
  --on: #ff4d1f;                    /* 계정마다 다른 채움 색 */
  --on-ink: #1b1a20;
  display: grid;
  place-items: center;
  width: 28px; height: 28px;
  border-radius: 50%;
  background: #ffffff;
  color: #575162;
  transition: transform 220ms cubic-bezier(.2, 1.4, .4, 1),
              background-color 220ms ease-out, color 220ms ease-out;
}
.fo__dot--b { --on: #1b1a20; --on-ink: #ffffff; }
.fo__dot--c { --on: #0b7269; --on-ink: #ffffff; }

.fo__dot:hover,
.fo__dot:focus-visible {
  transform: translateY(-6px);
  background: var(--on);
  color: var(--on-ink);
}

04스티키 하단 앱바

화면 바닥에 붙은 막대의 네 칸 사이를 밝은 알약이 미끄러지고, 알약이 닿는 칸의 아이콘이 한 번 튑니다. 알약 폭을 픽셀로 짐작하지 않고 칸과 똑같이 25% 로 두었기 때문에 translateX(100%) 한 번이 정확히 한 칸이 됩니다. 손가락이 화면 아래쪽에 머무는 모바일 웹앱에 씁니다.

translateXaria-current:focus-visible
.dk__nav {
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
}

/* 칸 하나 = 알약 폭 = 25%. translateX(100%) 가 정확히 한 칸이다 */
.dk__pill {
  position: absolute; top: 0; left: 0; z-index: 0;
  width: 25%;
  height: 100%;
  border-radius: 12px;
  background: #6ee7d7;
  transform: translateX(calc(var(--n, 0) * 100%));
  transition: transform 300ms cubic-bezier(.2, .8, .2, 1);
}
.dk__tab { position: relative; z-index: 1; color: #b9b3c4; }
.dk__tab.is-on { color: #1b1a20; }   /* 아쿠아 알약 위 잉크 글자 11.58:1 */

05소셜 타일 푸터

작은 아이콘 대신 손가락으로 누르기 쉬운 큰 타일 넷이 서고, 0.5초 간격으로 하나씩 6px 들렸다 앉습니다. 들린 타일은 그림자가 깊어지고 아이콘이 계정 고유색으로 바뀌며 모서리에 화살표가 scale(0) 에서 자라납니다. 팔로우를 유도하는 크리에이터 사이트에 씁니다.

translateYbox-shadow:focus-visible
/* 들림은 빠르게(0~5%), 앉음은 느리게(16~25%) 둔다 */
@keyframes tileLift {
  0%        { transform: translateY(0);    box-shadow: 0 6px 12px rgba(23, 20, 26, .32); }
  5%, 16%   { transform: translateY(-6px); box-shadow: 0 14px 24px rgba(23, 20, 26, .32); }
  25%, 100% { transform: translateY(0);    box-shadow: 0 6px 12px rgba(23, 20, 26, .32); }
}

/* 모서리의 열림 표시 — scale(0) 이라 쉴 때는 픽셀이 아예 없다 */
@keyframes tileGo {
  0%        { transform: scale(0); }
  5%, 16%   { transform: scale(1); }
  25%, 100% { transform: scale(0); }
}

/* 시차는 음수 delay 로. animation 단축을 쓰면 delay 가 0 으로 리셋된다 */
.is-demo .sc__tile:nth-child(2) { animation-delay: -1.5s; }
.is-demo .sc__tile:nth-child(3) { animation-delay: -1s; }
.is-demo .sc__tile:nth-child(4) { animation-delay: -.5s; }

06법적·링크 트리 푸터

중요한 링크일수록 글자가 커지는 계단을 만들고, 약관과 정책은 그 아래 작은 줄로 따로 모았습니다. 마우스를 얹으면 링크가 오른쪽으로 6px 밀리면서 밑줄이 왼쪽에서 오른쪽으로 차오르는데, 폭이 아니라 scaleX 라서 글자 위치를 다시 계산하지 않습니다. 링크 하나로 모으는 프로필형 하단에 씁니다.

scaleXfont-size:focus-visible
.lt__link--lead { font-weight: 800; font-size: clamp(20px, 5.4vw, 26px); line-height: 1.15; }
.lt__link--sub  { font-weight: 600; font-size: clamp(14px, 3.6vw, 17px); line-height: 1.2; }

/* 밑줄은 폭이 아니라 transform 이라 레이아웃을 다시 계산하지 않는다 */
.lt__link::after {
  content: "";
  position: absolute; left: 0; bottom: 0;
  width: 100%; height: 3px;
  border-radius: 999px;
  background: #1b1a20;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 220ms ease-out;
}
.lt__link:hover::after,
.lt__link:focus-visible::after { transform: scaleX(1); }
.lt__link[aria-current="page"] { font-weight: 900; }

07연락처·지도 카드 푸터

왼쪽 지도에 핀이 떨어져 한 번 튀고 동그란 물결이 퍼지는 동안, 오른쪽의 주소·전화·메일 카드가 220ms 간격으로 차례로 드러납니다. 지도의 도로 무늬는 이미지 파일이 아니라 repeating-linear-gradient 세 겹이라 외부 요청이 0건입니다. 매장이나 사무실 위치를 알리는 푸터에 씁니다.

repeating-linear-gradienttranslateYscale
/* 도로는 그림이 아니라 그러데이션 세 겹. 타일 크기를 먼저 못 박는다 */
.fc__map {
  background-color: #fbf6ee;
  background-image:
    repeating-linear-gradient(0deg,  rgba(87, 81, 98, .20) 0 6px, transparent 6px 72px),
    repeating-linear-gradient(90deg, rgba(87, 81, 98, .16) 0 2px, transparent 2px 36px),
    repeating-linear-gradient(0deg,  rgba(87, 81, 98, .16) 0 2px, transparent 2px 36px);
  background-size: 72px 72px, 36px 36px, 36px 36px;
}

/* 카드는 투명도가 아니라 clip-path 로 왼쪽부터 드러나고 왼쪽부터 걷힌다 */
@keyframes cardIn {
  0%, 6%    { transform: translateY(10px); clip-path: inset(0 100% 0 0); }
  26%       { transform: translateY(0);    clip-path: inset(0 0 0 0); }
  86%       { transform: translateY(0);    clip-path: inset(0 0 0 0); }
  99%, 100% { transform: translateY(0);    clip-path: inset(0 0 0 100%); }
}

08웨이브 경계 푸터

밝은 본문과 어두운 푸터가 만나는 28px 띠 안에서 물결 두 겹이 서로 다른 속도로 흐릅니다. 물결 모양은 SVG 를 data URI 로 깐 배경이라 파일을 따로 올리지 않아도 되고, 데모 안의 스위치를 누르면 animation-play-state 로 흐름만 멈춥니다. 섹션 색이 바뀌는 지점을 부드럽게 마감할 때 씁니다.

translateXprefers-reduced-motion:focus-visible
.wv__wave {
  position: absolute; top: 0; left: 0;
  width: 960px;                   /* 한 벌 480px(타일 4장) × 2 */
  height: 28px;
  background-repeat: repeat-x;
  background-size: 120px 28px;    /* 타일을 먼저 못 박는다 */
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  animation-duration: 2s;
}

/* 이동량은 타일 폭(120px)의 정수배 — 한 바퀴 뒤 무늬가 픽셀까지 같다 */
@keyframes waveBack  { from { transform: translateX(0); } to { transform: translateX(-240px); } }
@keyframes waveFront { from { transform: translateX(0); } to { transform: translateX(-480px); } }

.wv.is-paused .wv__wave { animation-play-state: paused; }

09다크 잉크 대비 푸터

밝은 본문 카드가 어두운 잉크 바닥 위 20px 에서 내려앉아 한 번 살짝 튀고, 그다음 푸터 링크 넷에 아쿠아 밑줄이 180ms 간격으로 차례로 그어집니다. 카드 바깥 바닥이 무대와 같은 잉크색이라 카드가 진짜로 바닥에 얹힌 것처럼 보입니다. 밝은 톤 사이트의 맨 끝을 묵직하게 닫을 때 씁니다.

scaleXtranslateY:focus-visible
.ic { --lift: -20px; }              /* 본문 카드가 내려앉기 시작하는 높이 */

@keyframes sheetDrop {
  0%        { transform: translateY(var(--lift)); }
  22%       { transform: translateY(0); }
  30%       { transform: translateY(-3px); }
  38%, 100% { transform: translateY(0); }
}

/* 복귀는 90% 안에 끝낸다 — 24프레임 샘플의 마지막 장이 95.8% 지점이다 */
@keyframes ruleScan {
  0%        { transform: scaleX(0); }
  24%, 60%  { transform: scaleX(1); }
  84%, 100% { transform: scaleX(0); }
}
.ic.is-demo .ic__link:nth-child(2) .ic__rule { animation-delay: 180ms; }
.ic.is-demo .ic__link:nth-child(3) .ic__rule { animation-delay: 360ms; }
.ic.is-demo .ic__link:nth-child(4) .ic__rule { animation-delay: 540ms; }

어디서 깨지는가 — 함정

가장 먼저 무너진 것은 세로였습니다. 폰 폭 렌더의 무대는 320×200 이고, 위아래 패딩 12.8px 을 두 번 빼면 실제로 쓸 수 있는 세로는 174.4px 뿐입니다. 01 다단 푸터의 첫 설계는 좁은 화면에서 단마다 링크를 세로로 쌓는 흔한 처방을 따랐는데, 그 결과가 176.4px 로 2px 넘쳤습니다. 좁아졌으니 세로로 쌓는다는 반사적인 대응이 오히려 높이를 키운 것입니다. 링크 목록을 flex-direction: row; flex-wrap: wrap 으로 눕히고 여백을 한 단계씩 줄여 160px 로 내렸습니다. 좁은 화면 안에서는 값을 올리지 않고 내린다는 규칙이 여기서 나왔습니다.

두 번째는 썸네일입니다. 대표 이미지는 앞 장과 가장 크게 달라진 한 장으로 뽑히는데, 그래서 등장보다 퇴장이 이기기 쉽습니다. 07 연락처 카드는 처음에 카드 셋을 opacity 로 띄웠습니다. cubic-bezier(.2, .8, .2, 1) 곡선은 구간 30% 지점에서 이미 opacity 가 0.827 이라, 24프레임 샘플에 반투명한 유령 장이 반드시 한 장 걸렸습니다. 게다가 카드가 등장하는 프레임의 변화 면적 14,625px 가 그다음 장의 13,669px 를 이겨, 세 번째 카드가 아직 없는 장이 대표로 뽑혔습니다. 투명도를 버리고 clip-path: inset() 으로 왼쪽부터 드러나게 바꾸니 변화가 프레임마다 고르게 나뉘어 카드 셋과 핀과 물결이 다 있는 장이 대표가 되었습니다. 06 링크 트리도 같은 이유로 밑줄 차오름 9%·물러남 5% 를 5%·9% 로 뒤집어, 밑줄이 그어진 장이 뽑히게 했습니다.

세 번째는 이음매입니다. 08 웨이브의 트랙 폭은 Playwright 의 offsetWidth 로 재어 960px, 물결 타일은 120px, 한 벌이 480px 로 타일 정확히 넉 장입니다. 앞겹은 −480px, 뒷겹은 −240px 로 밀어 둘 다 타일 폭의 정수배가 되게 했고, translateX 가 0 · −480 · −240 인 세 지점의 스크린샷 해시가 완전히 같고 −479px 에서만 달랐습니다. 한 바퀴를 돌아온 뒤 픽셀이 어긋나지 않는다는 증거입니다. 정수배를 어기면 2초마다 무늬가 한 번씩 튀는데, 그 튐은 눈에 잘 안 띄면서도 계속 거슬립니다.

마지막은 04 하단 앱바의 색입니다. 활성 칸의 글자색을 알약 색과 같게 두었더니, 알약이 옆 칸으로 옮겨 가는 프레임에서 잉크색 글자가 잉크색 막대 위에 남아 그 칸이 통째로 사라져 보였습니다. 알약의 도착 시각도 키프레임 끝이 아니었습니다 — cubic-bezier(.2, 1.4, .4, 1) 은 목표를 한 번 지나쳤다 돌아오는 곡선이라, 키프레임 끝인 22·46·70% 가 아니라 실측 도착 시각인 4·27·51·75% 에 아이콘 튐을 맞춰야 했습니다. 글자색 교체는 steps(1, end) 컷으로 바꾸고 그 창을 24프레임 눈금(4.1667% 간격)에 맞춰 잘랐습니다.

접근성

색은 짐작하지 않고 WCAG 2.1 상대 휘도 공식으로 직접 계산했습니다. 아래 표의 맨 아래 두 줄이 이 팔레트에서 쓰지 않기로 한 조합입니다.

실측 대비 본문 기준
흰 글자 #ffffff 잉크 #1b1a20 17.28:1 통과
앰버 #ffd23f 잉크 #1b1a20 11.97:1 통과
아쿠아 #6ee7d7 잉크 #1b1a20 11.58:1 통과
미스트 #b9b3c4 잉크 #1b1a20 8.48:1 통과
잉크 #1b1a20 크림 #fbf6ee 16.06:1 통과
스톤 #575162 크림 #fbf6ee 7.08:1 통과
흰 글자 #ffffff 틸 #0b7269 5.79:1 통과
잉크 #1b1a20 오렌지 #ff4d1f 5.21:1 통과
흰 글자 #ffffff 오렌지 #ff4d1f 3.32:1 미달 — 쓰지 않는다
흰 글자 #ffffff 앰버 #ffd23f 1.44:1 미달 — 쓰지 않는다

이 표를 만들며 통과한 조합만 골라 다시 렌더한 아홉 벌이 zip 에 그대로 들어 있고, 여는 비밀번호는 7g7q5d5z 입니다. 움직임을 줄여 달라는 설정(prefers-reduced-motion: reduce)에서는 아홉 개 모두 움직임만 끄고 상태와 정보는 남깁니다. 01 은 단이 펼쳐진 채로 서고 aria-expanded 가 그 상태를 그대로 알립니다. 02 는 아이콘·문구·진행 막대가 전부 멈추고 지금 상태 하나만 보입니다. 03 은 애니메이션과 전환이 함께 꺼져 점 색이 즉시 바뀝니다. 04 는 알약과 아이콘이 멈춰도 .dk__tab.is-on 규칙이 남아 활성 칸 하나는 그대로 잉크색입니다. 05 는 타일이 서 있되 호버·초점의 화살표 표시는 살아 있어 어느 것이 열리는 링크인지 알 수 있습니다. 06 은 aria-current="page" 링크의 밑줄이 차 있는 채로 멈춥니다. 07 은 카드 셋이 모두 제자리에 서고 물결 고리만 얇게 남습니다. 08 은 물결이 멈추되 곡선 모양은 그대로입니다. 09 는 카드가 바닥에 얹힌 상태로 고정됩니다.

구조 쪽에서 실제로 넣은 장치는 이렇습니다. 아홉 개 전부 <footer> 로 감싸 contentinfo 랜드마크를 만들었고, 03 처럼 다른 요소 안에 들어갈 수 있는 데모에는 role="contentinfo" 를 명시했습니다. 링크 묶음에는 <nav aria-label="푸터 링크"> 처럼 이름을 붙여 스크린 리더 목록에서 헤더의 메뉴와 구분되도록 했습니다. 01 의 아코디언 버튼은 aria-expandedaria-controls 를 함께 쓰고 위아래 화살표 키로 열 머리 사이를 옮길 수 있습니다. 04 와 06 은 현재 위치에 aria-current="page" 를 답니다. 02 의 결과 문장은 role="status"aria-live="polite" 를 단 한 칸에 모아 두어, 성공이든 오류든 화면을 못 보는 사람에게 같은 자리에서 한 번만 읽힙니다. 08 의 재생 스위치는 aria-pressed 로 상태를 알리고, 07 의 연락처는 <address> 안에 있습니다. 아홉 개 모두 :focus-visible 아웃라인을 무대마다 보이는 색으로 따로 지정했습니다 — 04 는 아쿠아 알약 위에서도 보이도록 흰 테를 씁니다. 여닫기 방식의 근거는 MDN 의 grid-template-rows 문서aria-live 리전 문서, 대비 기준은 W3C 의 최소 명도 대비 해설을 따랐습니다. 화면 아래에 붙는 조작바만 더 보고 싶다면 플로팅 독 내비게이션내비게이션 모음으로 이어집니다. 이 사이트가 코드를 어떻게 만들고 검증하는지는 소개에 적어 두었습니다.

FAQ

푸터에 링크를 몇 개까지 넣어도 괜찮나요?

개수보다 묶음이 중요합니다. 01 다단 사이트맵처럼 제품·회사·지원 같은 머리를 세 개쯤 두고 그 밑에 서너 개씩 담으면 링크가 스무 개여도 읽힙니다. 반대로 묶음 없이 열두 개를 한 줄로 늘어놓으면 다섯 개일 때보다 더 안 읽힙니다. 좁은 화면에서 묶음이 아코디언으로 접히면 세로 높이 문제도 같이 풀립니다.

푸터를 화면 아래에 계속 붙여 둬도 되나요?

04 처럼 바닥에 고정하는 것은 푸터가 아니라 조작바로 쓸 때만 맞습니다. 실제로 04 의 막대는 position: sticky; bottom: 0 이고, 그 sticky 가 걸리려면 부모 문서가 화면보다 길어야 합니다. 여유를 padding-bottom 으로 주면 sticky 범위 밖이라 소용이 없어, 데모에서는 문서의 min-height 를 280px 로 잡아 높이 차이를 만들었습니다. 저작권과 사이트맵 같은 진짜 푸터 내용은 문서 끝에 그냥 두는 편이 낫습니다.

어두운 푸터에서 글자색은 어떻게 고르나요?

밝기가 아니라 대비비로 고릅니다. 위 표에서 보듯 잉크 바닥 위의 미스트 회색은 8.48:1 이라 보조 글자로 충분하지만, 같은 흰 글자라도 오렌지 위에서는 3.32:1 로 떨어져 본문 기준 4.5:1 을 못 넘깁니다. 그래서 이 아홉 개는 오렌지 무대 위의 글자를 전부 잉크로 뒤집었습니다. 강조색을 정할 때는 색을 먼저 고르고 대비를 재는 대신, 대비를 먼저 재고 통과한 것 중에서 고르는 편이 빠릅니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.