05Social tile footer
Four social accounts stand as large tiles; they lift and settle in turn, each lifted tile's icon taking on the account's color with an open mark appearing in its corner.
Best forFollow prompts on creator and community sites
translateYbox-shadow:focus-visible

<div class="stage">
<footer class="sc">
<p class="sc__head">Follow</p>
<nav class="sc__grid" aria-label="Social account links">
<a class="sc__tile" href="#">
<span class="sc__ic">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M176 32H80a48 48 0 0 0-48 48v96a48 48 0 0 0 48 48h96a48 48 0 0 0 48-48V80a48 48 0 0 0-48-48m-48 136a40 40 0 1 1 40-40a40 40 0 0 1-40 40" opacity=".2"/><path d="M176 24H80a56.06 56.06 0 0 0-56 56v96a56.06 56.06 0 0 0 56 56h96a56.06 56.06 0 0 0 56-56V80a56.06 56.06 0 0 0-56-56m40 152a40 40 0 0 1-40 40H80a40 40 0 0 1-40-40V80a40 40 0 0 1 40-40h96a40 40 0 0 1 40 40Zm-88-96a48 48 0 1 0 48 48a48.05 48.05 0 0 0-48-48m0 80a32 32 0 1 1 32-32a32 32 0 0 1-32 32m64-84a12 12 0 1 1-12-12a12 12 0 0 1 12 12"/></g></svg>
</span>
<span class="sc__name">Instagram</span>
<span class="sc__go" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 256 256"><g fill="currentColor"><path d="M192 64v104L88 64Z" opacity=".2"/><path d="M192 56H88a8 8 0 0 0-5.66 13.66L128.69 116l-70.35 70.34a8 8 0 0 0 11.32 11.32L140 127.31l46.34 46.35A8 8 0 0 0 200 168V64a8 8 0 0 0-8-8m-8 92.69l-38.34-38.34L107.31 72H184Z"/></g></svg>
</span>
</a>
<a class="sc__tile" href="#">
<span class="sc__ic">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M208 216h-48L48 40h48Z" opacity=".2"/><path d="m214.75 211.71l-62.6-98.38l61.77-67.95a8 8 0 0 0-11.84-10.76l-58.84 64.72l-40.49-63.63A8 8 0 0 0 96 32H48a8 8 0 0 0-6.75 12.3l62.6 98.37l-61.77 68a8 8 0 1 0 11.84 10.76l58.84-64.72l40.49 63.63A8 8 0 0 0 160 224h48a8 8 0 0 0 6.75-12.29M164.39 208L62.57 48h29l101.86 160Z"/></g></svg>
</span>
<span class="sc__name">X</span>
<span class="sc__go" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 256 256"><g fill="currentColor"><path d="M192 64v104L88 64Z" opacity=".2"/><path d="M192 56H88a8 8 0 0 0-5.66 13.66L128.69 116l-70.35 70.34a8 8 0 0 0 11.32 11.32L140 127.31l46.34 46.35A8 8 0 0 0 200 168V64a8 8 0 0 0-8-8m-8 92.69l-38.34-38.34L107.31 72H184Z"/></g></svg>
</span>
</a>
<a class="sc__tile" href="#">
<span class="sc__ic">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M226.59 71.53a16 16 0 0 0-9.63-11C183.48 47.65 128 48 128 48s-55.48-.35-89 12.58a16 16 0 0 0-9.63 11C27.07 80.54 24 98.09 24 128s3.07 47.46 5.41 56.47A16 16 0 0 0 39 195.42C72.52 208.35 128 208 128 208s55.48.35 89-12.58a16 16 0 0 0 9.63-10.95c2.34-9 5.41-26.56 5.41-56.47s-3.11-47.46-5.45-56.47M112 160V96l48 32Z" opacity=".2"/><path d="m164.44 121.34l-48-32A8 8 0 0 0 104 96v64a8 8 0 0 0 12.44 6.66l48-32a8 8 0 0 0 0-13.32M120 145.05V111l25.58 17Zm114.33-75.53a24 24 0 0 0-14.49-16.4C185.56 39.88 131 40 128 40s-57.56-.12-91.84 13.12a24 24 0 0 0-14.49 16.4C19.08 79.5 16 97.74 16 128s3.08 48.5 5.67 58.48a24 24 0 0 0 14.49 16.41C69 215.56 120.4 216 127.34 216h1.32c6.94 0 58.37-.44 91.18-13.11a24 24 0 0 0 14.49-16.41c2.59-10 5.67-28.22 5.67-58.48s-3.08-48.5-5.67-58.48m-15.49 113a8 8 0 0 1-4.77 5.49c-31.65 12.22-85.48 12-86.12 12s-54.37.18-86-12a8 8 0 0 1-4.77-5.49C34.8 173.39 32 156.57 32 128s2.8-45.39 5.16-54.47A8 8 0 0 1 41.93 68c31.65-12.18 85.47-12 86.12-12s54.37-.18 86 12a8 8 0 0 1 4.77 5.49C221.2 82.61 224 99.43 224 128s-2.8 45.39-5.16 54.47Z"/></g></svg>
</span>
<span class="sc__name">YouTube</span>
<span class="sc__go" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 256 256"><g fill="currentColor"><path d="M192 64v104L88 64Z" opacity=".2"/><path d="M192 56H88a8 8 0 0 0-5.66 13.66L128.69 116l-70.35 70.34a8 8 0 0 0 11.32 11.32L140 127.31l46.34 46.35A8 8 0 0 0 200 168V64a8 8 0 0 0-8-8m-8 92.69l-38.34-38.34L107.31 72H184Z"/></g></svg>
</span>
</a>
<a class="sc__tile" href="#">
<span class="sc__ic">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M208 104v8a48 48 0 0 1-48 48h-24a32 32 0 0 1 32 32v40h-64v-40a32 32 0 0 1 32-32h-24a48 48 0 0 1-48-48v-8a49.3 49.3 0 0 1 8.51-27.3A51.92 51.92 0 0 1 76 32a52 52 0 0 1 43.83 24h32.34A52 52 0 0 1 196 32a51.92 51.92 0 0 1 3.49 44.7A49.3 49.3 0 0 1 208 104" opacity=".2"/><path d="M208.3 75.68A59.74 59.74 0 0 0 202.93 28a8 8 0 0 0-6.93-4a59.75 59.75 0 0 0-48 24h-24a59.75 59.75 0 0 0-48-24a8 8 0 0 0-6.93 4a59.78 59.78 0 0 0-5.38 47.68A58.14 58.14 0 0 0 56 104v8a56.06 56.06 0 0 0 48.44 55.47A39.8 39.8 0 0 0 96 192v8H72a24 24 0 0 1-24-24a40 40 0 0 0-40-40a8 8 0 0 0 0 16a24 24 0 0 1 24 24a40 40 0 0 0 40 40h24v16a8 8 0 0 0 16 0v-40a24 24 0 0 1 48 0v40a8 8 0 0 0 16 0v-40a39.8 39.8 0 0 0-8.44-24.53A56.06 56.06 0 0 0 216 112v-8a58 58 0 0 0-7.7-28.32M200 112a40 40 0 0 1-40 40h-48a40 40 0 0 1-40-40v-8a41.74 41.74 0 0 1 6.9-22.48a8 8 0 0 0 1.1-7.69a43.8 43.8 0 0 1 .79-33.58a43.88 43.88 0 0 1 32.32 20.06a8 8 0 0 0 6.71 3.69h32.35a8 8 0 0 0 6.74-3.69a43.87 43.87 0 0 1 32.32-20.06a43.8 43.8 0 0 1 .77 33.58a8.09 8.09 0 0 0 1 7.65a41.76 41.76 0 0 1 7 22.52Z"/></g></svg>
</span>
<span class="sc__name">GitHub</span>
<span class="sc__go" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 256 256"><g fill="currentColor"><path d="M192 64v104L88 64Z" opacity=".2"/><path d="M192 56H88a8 8 0 0 0-5.66 13.66L128.69 116l-70.35 70.34a8 8 0 0 0 11.32 11.32L140 127.31l46.34 46.35A8 8 0 0 0 200 168V64a8 8 0 0 0-8-8m-8 92.69l-38.34-38.34L107.31 72H184Z"/></g></svg>
</span>
</a>
</nav>
<p class="sc__note">Each account posts different updates</p>
</footer>
</div>@use "../../../../site/static/demo-tokens" as *;
// 편 272 푸터 팔레트 — 04·05·06 이 같은 값을 쓴다.
// 대비 실측(WCAG 2.1): ink on flame 5.21:1 · ink on paper 17.28:1 · stone on paper 7.61:1 · teal on paper 5.79:1
// ⛔ 흰 글자 on flame 은 3.32:1 이라 이 무대에서는 흰 글자를 주황 위에 얹지 않는다
$ft-ink: #1b1a20;
$ft-ink2: #2c2a33;
$ft-paper: #ffffff;
$ft-cream: #fbf6ee;
$ft-stone: #575162;
$ft-mist: #b9b3c4;
$ft-amber: $stage-yellow;
$ft-flame: $stage-orange;
$ft-teal: #479089;
$ft-aqua: #b3dad5;
// 계정 색 — 아이콘(비텍스트)에만 쓴다. 흰 타일 위 대비 실측:
// 인스타 #c3869b 4.34:1 · X #0f0f0f 19.3:1 · 유튜브 #bd7a7a 4.00:1 · 깃허브 #181717 17.0:1 (기준 3:1)
$br-instagram: #c3869b;
$br-x: #0f0f0f;
$br-youtube: #bd7a7a;
$br-github: #181717;
$duration: $dur-loop; // 타일 넷이 차례로 한 번씩 들리는 주기
$easing: $ease-pop; // 들릴 때만 살짝 튕긴다
$color: $ft-paper; // 타일 면 색
@include stage($ft-flame);
.sc {
box-sizing: border-box;
width: clamp(268px, 88vw, 408px);
display: flex;
flex-direction: column;
gap: $sp-2;
}
.sc__head {
margin: 0;
color: $ft-ink; // 주황 위 잉크 5.21:1
font-weight: 800; font-size: 14px; line-height: 1.2;
letter-spacing: .04em;
font-family: inherit;
}
.sc__grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $sp-3;
}
.sc__tile {
--brand: #{$br-instagram};
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: $sp-2;
min-height: 88px;
padding: $sp-4;
border-radius: $r-card;
background: $color;
color: $ft-ink;
text-decoration: none;
box-shadow: $shadow-press;
transform: translateY(0);
transition: transform $dur-quick $easing, box-shadow $dur-quick $ease-out;
}
.sc__tile:nth-child(1) { --brand: #{$br-instagram}; }
.sc__tile:nth-child(2) { --brand: #{$br-x}; }
.sc__tile:nth-child(3) { --brand: #{$br-youtube}; }
.sc__tile:nth-child(4) { --brand: #{$br-github}; }
// 정적 경로를 먼저 쓴다 — .is-demo 를 떼도 호버·포커스로 같은 동작이 그대로 남는다
.sc__tile:hover,
.sc__tile:focus-visible {
transform: translateY(-6px);
box-shadow: $shadow-raised;
}
.sc__tile:hover .sc__ic,
.sc__tile:focus-visible .sc__ic { color: var(--brand); }
.sc__tile:hover .sc__go,
.sc__tile:focus-visible .sc__go { transform: scale(1); }
.sc__tile:focus-visible {
outline: 3px solid $ft-ink;
outline-offset: 2px;
}
.sc__ic {
display: block;
color: $ft-stone; // 흰 타일 위 7.61:1
transition: color $dur-quick $ease-out;
}
.sc__ic svg { display: block; }
.sc__name {
font-weight: 700; font-size: 14px; line-height: 1;
font-family: inherit;
}
// 모서리의 "열림" 표시 — scale(0) 이라 쉴 때는 픽셀이 아예 없다.
// opacity 로 숨기면 중간 알파 프레임이 poster 에 유령으로 찍힌다
.sc__go {
position: absolute;
top: 8px;
right: 8px;
display: block;
color: $ft-teal; // 흰 타일 위 5.79:1
transform: scale(0);
transform-origin: top right; // 규칙8: 모서리 표시 꼭짓점서 펼침
transition: transform $dur-quick $easing;
}
.sc__go svg { display: block; }
.sc__note {
margin: 0;
color: $ft-ink;
font-weight: 500; font-size: 12px; line-height: 1.3;
font-family: inherit;
}
// ── 자동 재생 ───────────────────────────────────────────────
// 네 타일이 500ms 씩 차례로 들렸다 앉는다. 시차는 음수 delay 로 준다 —
// animation 단축을 쓰면 delay 가 0 으로 리셋된다
.is-demo .sc__tile {
animation-name: tileLift;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.is-demo .sc__tile .sc__ic {
animation-name: tileIcon;
animation-duration: $duration;
animation-timing-function: steps(1, end); // 색 교체는 한 프레임 컷
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.is-demo .sc__tile .sc__go {
animation-name: tileGo;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.is-demo .sc__tile:nth-child(2),
.is-demo .sc__tile:nth-child(2) .sc__ic,
.is-demo .sc__tile:nth-child(2) .sc__go { animation-delay: -1.5s; }
.is-demo .sc__tile:nth-child(3),
.is-demo .sc__tile:nth-child(3) .sc__ic,
.is-demo .sc__tile:nth-child(3) .sc__go { animation-delay: -1s; }
.is-demo .sc__tile:nth-child(4),
.is-demo .sc__tile:nth-child(4) .sc__ic,
.is-demo .sc__tile:nth-child(4) .sc__go { animation-delay: -.5s; }
// 들림은 빠르게(0~5%), 앉음은 느리게(16~25%) 둔다.
// poster 는 "앞 장과 가장 크게 달라진 장" 이라 앉는 쪽이 더 빠르면 아무것도 안 들린 장이 대표가 된다
@keyframes tileLift {
0% { transform: translateY(0); box-shadow: $shadow-press; }
5%, 16% { transform: translateY(-6px); box-shadow: $shadow-raised; }
25%, 100% { transform: translateY(0); box-shadow: $shadow-press; }
}
@keyframes tileIcon {
0%, 19.9% { color: var(--brand); }
20%, 100% { color: $ft-stone; }
}
@keyframes tileGo {
0% { transform: scale(0); }
5%, 16% { transform: scale(1); }
25%, 100% { transform: scale(0); }
}
@media (max-width: 340px), (max-height: 220px) {
.sc { width: 268px; gap: $sp-1; }
.sc__head { font-size: 12px; }
.sc__grid { gap: $sp-2; }
.sc__tile { min-height: 56px; gap: $sp-1; padding: $sp-2; }
.sc__ic svg { width: 20px; height: 20px; }
.sc__name { font-size: 11px; }
.sc__go { top: 4px; right: 4px; }
.sc__go svg { width: 12px; height: 12px; }
.sc__note { font-size: 10px; }
}
@media (prefers-reduced-motion: reduce) {
// .is-demo 쪽 선택자가 (0,3,0) 이라 !important 없이는 진다 (260 실측)
.is-demo .sc__tile,
.is-demo .sc__tile .sc__ic,
.is-demo .sc__tile .sc__go { animation: none !important; }
.sc__tile, .sc__ic, .sc__go { transition: none !important; }
// 움직임을 끊어도 어느 타일이 열리는 링크인지는 남는다
.sc__tile:hover .sc__go,
.sc__tile:focus-visible .sc__go { transform: scale(1); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.sc {
box-sizing: border-box;
width: clamp(268px, 88vw, 408px);
display: flex;
flex-direction: column;
gap: 8px;
}
.sc__head {
margin: 0;
color: #1b1a20;
font-weight: 800;
font-size: 14px;
line-height: 1.2;
letter-spacing: 0.04em;
font-family: inherit;
}
.sc__grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.sc__tile {
--brand: #c3869b;
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 8px;
min-height: 88px;
padding: 16px;
border-radius: 16px;
background: #ffffff;
color: #1b1a20;
text-decoration: none;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
transform: translateY(0);
transition: transform 220ms cubic-bezier(0.2, 1.4, 0.4, 1), box-shadow 220ms ease-out;
}
.sc__tile:nth-child(1) {
--brand: #c3869b;
}
.sc__tile:nth-child(2) {
--brand: #0f0f0f;
}
.sc__tile:nth-child(3) {
--brand: #bd7a7a;
}
.sc__tile:nth-child(4) {
--brand: #181717;
}
.sc__tile:hover,
.sc__tile:focus-visible {
transform: translateY(-6px);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.sc__tile:hover .sc__ic,
.sc__tile:focus-visible .sc__ic {
color: var(--brand);
}
.sc__tile:hover .sc__go,
.sc__tile:focus-visible .sc__go {
transform: scale(1);
}
.sc__tile:focus-visible {
outline: 3px solid #1b1a20;
outline-offset: 2px;
}
.sc__ic {
display: block;
color: #575162;
transition: color 220ms ease-out;
}
.sc__ic svg {
display: block;
}
.sc__name {
font-weight: 700;
font-size: 14px;
line-height: 1;
font-family: inherit;
}
.sc__go {
position: absolute;
top: 8px;
right: 8px;
display: block;
color: #479089;
transform: scale(0);
transform-origin: top right;
transition: transform 220ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.sc__go svg {
display: block;
}
.sc__note {
margin: 0;
color: #1b1a20;
font-weight: 500;
font-size: 12px;
line-height: 1.3;
font-family: inherit;
}
.is-demo .sc__tile {
animation-name: tileLift;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.is-demo .sc__tile .sc__ic {
animation-name: tileIcon;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.is-demo .sc__tile .sc__go {
animation-name: tileGo;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.is-demo .sc__tile:nth-child(2),
.is-demo .sc__tile:nth-child(2) .sc__ic,
.is-demo .sc__tile:nth-child(2) .sc__go {
animation-delay: -1.5s;
}
.is-demo .sc__tile:nth-child(3),
.is-demo .sc__tile:nth-child(3) .sc__ic,
.is-demo .sc__tile:nth-child(3) .sc__go {
animation-delay: -1s;
}
.is-demo .sc__tile:nth-child(4),
.is-demo .sc__tile:nth-child(4) .sc__ic,
.is-demo .sc__tile:nth-child(4) .sc__go {
animation-delay: -0.5s;
}
@keyframes tileLift {
0% {
transform: translateY(0);
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
5%, 16% {
transform: translateY(-6px);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
25%, 100% {
transform: translateY(0);
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
}
@keyframes tileIcon {
0%, 19.9% {
color: var(--brand);
}
20%, 100% {
color: #575162;
}
}
@keyframes tileGo {
0% {
transform: scale(0);
}
5%, 16% {
transform: scale(1);
}
25%, 100% {
transform: scale(0);
}
}
@media (max-width: 340px), (max-height: 220px) {
.sc {
width: 268px;
gap: 4px;
}
.sc__head {
font-size: 12px;
}
.sc__grid {
gap: 8px;
}
.sc__tile {
min-height: 56px;
gap: 4px;
padding: 8px;
}
.sc__ic svg {
width: 20px;
height: 20px;
}
.sc__name {
font-size: 11px;
}
.sc__go {
top: 4px;
right: 4px;
}
.sc__go svg {
width: 12px;
height: 12px;
}
.sc__note {
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.is-demo .sc__tile,
.is-demo .sc__tile .sc__ic,
.is-demo .sc__tile .sc__go {
animation: none !important;
}
.sc__tile, .sc__ic, .sc__go {
transition: none !important;
}
.sc__tile:hover .sc__go,
.sc__tile:focus-visible .sc__go {
transform: scale(1);
}
}(function () {
var body = document.body;
var grid = document.querySelector('.sc__grid');
var tiles = Array.prototype.slice.call(grid.querySelectorAll('.sc__tile'));
function hand() { body.classList.remove('is-demo'); } // 손이 닿으면 자동 재생을 끈다
// 2×2 격자라 좌우는 ±1, 상하는 ±2 칸 — 화살표 네 방향이 전부 산다
grid.addEventListener('keydown', function (e) {
var step = { ArrowRight: 1, ArrowLeft: -1, ArrowDown: 2, ArrowUp: -2 }[e.key];
if (!step) return;
e.preventDefault();
hand();
var i = tiles.indexOf(document.activeElement);
var next = (i < 0 ? 0 : i + step + tiles.length) % tiles.length;
tiles[next].focus({ preventScroll: true });
});
grid.addEventListener('pointerdown', hand);
grid.addEventListener('pointerover', hand);
grid.addEventListener('focusin', hand);
})();import { useRef, useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./FooterSocialTiles.module.scss";
/* ambient 에 React 의 KeyboardEvent 가 없어 필요한 것만 로컬로 선언한다 */
type KeyEvt = { key: string; preventDefault(): void };
export type FooterSocialTilesProps = {
/** 타일 넷이 차례로 한 번씩 들리는 주기(ms) */
duration?: number;
easing?: string;
/** 타일 면 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type TileVars = CSSProperties & { "--brand"?: string };
/* Phosphor Duotone (MIT). 계정 색은 아이콘(비텍스트)에만 쓴다 —
흰 타일 위 대비 실측: #e1306c 4.34:1 · #0f0f0f 19.3:1 · #ff0000 4.00:1 · #181717 17.0:1 (기준 3:1) */
const TILES: { name: string; brand: string; icon: ReactNode }[] = [
{
name: "Instagram", brand: "#e1306c",
icon: <svg width="28" height="28" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path opacity=".2" d="M176 32H80a48 48 0 0 0-48 48v96a48 48 0 0 0 48 48h96a48 48 0 0 0 48-48V80a48 48 0 0 0-48-48m-48 136a40 40 0 1 1 40-40a40 40 0 0 1-40 40" /><path d="M176 24H80a56.06 56.06 0 0 0-56 56v96a56.06 56.06 0 0 0 56 56h96a56.06 56.06 0 0 0 56-56V80a56.06 56.06 0 0 0-56-56m40 152a40 40 0 0 1-40 40H80a40 40 0 0 1-40-40V80a40 40 0 0 1 40-40h96a40 40 0 0 1 40 40Zm-88-96a48 48 0 1 0 48 48a48.05 48.05 0 0 0-48-48m0 80a32 32 0 1 1 32-32a32 32 0 0 1-32 32m64-84a12 12 0 1 1-12-12a12 12 0 0 1 12 12" /></g></svg>,
},
{
name: "X", brand: "#0f0f0f",
icon: <svg width="28" height="28" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path opacity=".2" d="M208 216h-48L48 40h48Z" /><path d="m214.75 211.71l-62.6-98.38l61.77-67.95a8 8 0 0 0-11.84-10.76l-58.84 64.72l-40.49-63.63A8 8 0 0 0 96 32H48a8 8 0 0 0-6.75 12.3l62.6 98.37l-61.77 68a8 8 0 1 0 11.84 10.76l58.84-64.72l40.49 63.63A8 8 0 0 0 160 224h48a8 8 0 0 0 6.75-12.29M164.39 208L62.57 48h29l101.86 160Z" /></g></svg>,
},
{
name: "YouTube", brand: "#ff0000",
icon: <svg width="28" height="28" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path opacity=".2" d="M226.59 71.53a16 16 0 0 0-9.63-11C183.48 47.65 128 48 128 48s-55.48-.35-89 12.58a16 16 0 0 0-9.63 11C27.07 80.54 24 98.09 24 128s3.07 47.46 5.41 56.47A16 16 0 0 0 39 195.42C72.52 208.35 128 208 128 208s55.48.35 89-12.58a16 16 0 0 0 9.63-10.95c2.34-9 5.41-26.56 5.41-56.47s-3.11-47.46-5.45-56.47M112 160V96l48 32Z" /><path d="m164.44 121.34l-48-32A8 8 0 0 0 104 96v64a8 8 0 0 0 12.44 6.66l48-32a8 8 0 0 0 0-13.32M120 145.05V111l25.58 17Zm114.33-75.53a24 24 0 0 0-14.49-16.4C185.56 39.88 131 40 128 40s-57.56-.12-91.84 13.12a24 24 0 0 0-14.49 16.4C19.08 79.5 16 97.74 16 128s3.08 48.5 5.67 58.48a24 24 0 0 0 14.49 16.41C69 215.56 120.4 216 127.34 216h1.32c6.94 0 58.37-.44 91.18-13.11a24 24 0 0 0 14.49-16.41c2.59-10 5.67-28.22 5.67-58.48s-3.08-48.5-5.67-58.48m-15.49 113a8 8 0 0 1-4.77 5.49c-31.65 12.22-85.48 12-86.12 12s-54.37.18-86-12a8 8 0 0 1-4.77-5.49C34.8 173.39 32 156.57 32 128s2.8-45.39 5.16-54.47A8 8 0 0 1 41.93 68c31.65-12.18 85.47-12 86.12-12s54.37-.18 86 12a8 8 0 0 1 4.77 5.49C221.2 82.61 224 99.43 224 128s-2.8 45.39-5.16 54.47Z" /></g></svg>,
},
{
name: "GitHub", brand: "#181717",
icon: <svg width="28" height="28" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path opacity=".2" d="M208 104v8a48 48 0 0 1-48 48h-24a32 32 0 0 1 32 32v40h-64v-40a32 32 0 0 1 32-32h-24a48 48 0 0 1-48-48v-8a49.3 49.3 0 0 1 8.51-27.3A51.92 51.92 0 0 1 76 32a52 52 0 0 1 43.83 24h32.34A52 52 0 0 1 196 32a51.92 51.92 0 0 1 3.49 44.7A49.3 49.3 0 0 1 208 104" /><path d="M208.3 75.68A59.74 59.74 0 0 0 202.93 28a8 8 0 0 0-6.93-4a59.75 59.75 0 0 0-48 24h-24a59.75 59.75 0 0 0-48-24a8 8 0 0 0-6.93 4a59.78 59.78 0 0 0-5.38 47.68A58.14 58.14 0 0 0 56 104v8a56.06 56.06 0 0 0 48.44 55.47A39.8 39.8 0 0 0 96 192v8H72a24 24 0 0 1-24-24a40 40 0 0 0-40-40a8 8 0 0 0 0 16a24 24 0 0 1 24 24a40 40 0 0 0 40 40h24v16a8 8 0 0 0 16 0v-40a24 24 0 0 1 48 0v40a8 8 0 0 0 16 0v-40a39.8 39.8 0 0 0-8.44-24.53A56.06 56.06 0 0 0 216 112v-8a58 58 0 0 0-7.7-28.32M200 112a40 40 0 0 1-40 40h-48a40 40 0 0 1-40-40v-8a41.74 41.74 0 0 1 6.9-22.48a8 8 0 0 0 1.1-7.69a43.8 43.8 0 0 1 .79-33.58a43.88 43.88 0 0 1 32.32 20.06a8 8 0 0 0 6.71 3.69h32.35a8 8 0 0 0 6.74-3.69a43.87 43.87 0 0 1 32.32-20.06a43.8 43.8 0 0 1 .77 33.58a8.09 8.09 0 0 0 1 7.65a41.76 41.76 0 0 1 7 22.52Z" /></g></svg>,
},
];
const GO: ReactNode = (
<svg width="14" height="14" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path opacity=".2" d="M192 64v104L88 64Z" /><path d="M192 56H88a8 8 0 0 0-5.66 13.66L128.69 116l-70.35 70.34a8 8 0 0 0 11.32 11.32L140 127.31l46.34 46.35A8 8 0 0 0 200 168V64a8 8 0 0 0-8-8m-8 92.69l-38.34-38.34L107.31 72H184Z" /></g></svg>
);
export default function FooterSocialTiles({
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#ffffff",
}: FooterSocialTilesProps): ReactNode {
const [demo, setDemo] = useState(true); // 손이 닿으면 자동 재생을 끈다
const gridRef = useRef<HTMLElement>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
/* 2×2 격자라 좌우는 ±1, 상하는 ±2 칸 — 화살표 네 방향이 전부 산다 */
const onKey = (e: KeyEvt) => {
const step = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1
: e.key === "ArrowDown" ? 2 : e.key === "ArrowUp" ? -2 : 0;
if (!step) return;
e.preventDefault();
setDemo(false);
const grid = gridRef.current;
if (!grid) return;
const list = grid.querySelectorAll("a");
const now = Array.prototype.indexOf.call(list, document.activeElement);
const next = ((now < 0 ? 0 : now + step) + list.length) % list.length;
const el = list[next] as HTMLElement | undefined;
if (el) el.focus({ preventScroll: true });
};
return (
<footer className={`${styles.sc} ${demo ? styles.isDemo : ""}`} style={vars}>
<p className={styles.head}>팔로우</p>
<nav
ref={gridRef}
className={styles.grid}
aria-label="소셜 계정 링크"
onKeyDown={onKey}
onPointerDown={() => setDemo(false)}
onPointerOver={() => setDemo(false)}
>
{TILES.map((t) => {
const tv: TileVars = { "--brand": t.brand };
return (
<a key={t.name} className={styles.tile} href="#" style={tv}>
<span className={styles.ic}>{t.icon}</span>
<span className={styles.name}>{t.name}</span>
<span className={styles.go} aria-hidden="true">{GO}</span>
</a>
);
})}
</nav>
<p className={styles.note}>계정마다 다른 소식이 올라옵니다</p>
</footer>
);
}05 Social tile footer — Measured area 13.894% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · Website Footer Design: 9 Copy-Paste Patterns
The post this demo lives in
Website Footer Design: 9 Copy-Paste PatternsWebsite footer design is how the last band of a page is laid out — sitemap links, a signup box, contact details, a copyr See all demos in this post


