03ミニマル一行フッター
ロゴと著作権、ソーシャルの点3つが一行に収まり、点の上を通るとアイコンが少し浮いてそれぞれの色に満ちます。
使いどころポートフォリオ・個人ブランドサイトの下部
translateYscaleX:focus-visible

<main class="stage">
<footer class="fo is-demo" role="contentinfo">
<a class="fo__mark" href="#">
<span class="fo__mark-t">スタジオ</span>
<span class="fo__line" aria-hidden="true"></span>
</a>
<p class="fo__copy">© 2026 スタジオ</p>
<nav class="fo__social" aria-label="ソーシャルリンク">
<a class="fo__dot fo__dot--a" href="#" aria-label="写真アカウント">
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true" focusable="false"><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>
</a>
<a class="fo__dot fo__dot--b" href="#" aria-label="短い投稿のアカウント">
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true" focusable="false"><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>
</a>
<a class="fo__dot fo__dot--c" href="#" aria-label="コードリポジトリ">
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true" focusable="false"><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>
</a>
</nav>
</footer>
</main>@use "../../../../site/static/demo-tokens" as *;
// 272 푸터 팔레트 — 세 항목이 같은 값을 쓴다. 괄호 안은 실측 대비비
$ft-ink: #1b1a20; // 워드마크 on cream 16.06:1
$ft-paper: $stage-paper; // 점 바탕 · ink/teal 면 위 아이콘
$ft-cream: #fbf6ee; // 무대
$ft-stone: #575162; // 저작권 on cream 7.08:1 · 점 아이콘 on paper 7.61:1
$ft-flame: $stage-orange; // 첫 점 면 (ink 아이콘 5.21:1)
$ft-teal: #479089; // 셋째 점 면 (흰 아이콘 5.79:1)
$duration: $dur-loop; // 점 세 개가 차례로 떠오르는 한 바퀴
$easing: $ease-pop; // 점이 떠오를 때의 튕김
$color: $ft-cream; // 무대(크림)
@include stage($ft-cream);
// 한 줄 그대로 닫힌다 — 좁은 화면에서도 세로로 쌓지 않는다 (쌓으면 오히려 세로로 넘친다)
.fo {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: $sp-3;
width: clamp(288px, 92vw, 432px);
padding-top: $sp-4;
border-top: 2px solid $ft-ink;
}
.fo__mark {
position: relative;
flex: 0 0 auto;
color: $ft-ink;
font-weight: 800; font-size: 20px; line-height: 1.1;
letter-spacing: -.01em;
text-decoration: none;
}
.fo__mark:focus-visible {
outline: 2px solid $ft-teal;
outline-offset: 4px;
border-radius: $r-xs;
}
// 이름 밑줄 — 왼쪽에서 오른쪽으로 차오른다
.fo__line {
position: absolute;
left: 0;
right: 0;
bottom: -4px;
height: 3px;
border-radius: $r-pill;
background: $ft-teal;
transform: scaleX(0);
transform-origin: left center; // 규칙8: 밑줄 왼쪽 채움
transition: transform $dur-base $easing;
}
.fo__mark:hover .fo__line,
.fo__mark:focus-visible .fo__line { transform: scaleX(1); }
.fo__copy {
flex: 0 1 auto;
margin: 0;
color: $ft-stone;
font-weight: 500; font-size: 12px; line-height: 1.2;
}
.fo__social {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: $sp-2;
}
// 정적 상태 — 흰 점에 돌 색 아이콘. 호버·포커스에서 떠오르며 저마다의 색으로 찬다
.fo__dot {
--on: #{$ft-flame};
--on-ink: #{$ft-ink};
display: grid;
place-items: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: $ft-paper;
color: $ft-stone;
transition: transform $dur-quick $easing, background-color $dur-quick $ease-out, color $dur-quick $ease-out;
}
.fo__dot--b { --on: #{$ft-ink}; --on-ink: #{$ft-paper}; }
.fo__dot--c { --on: #{$ft-teal}; --on-ink: #{$ft-paper}; }
.fo__dot:hover,
.fo__dot:focus-visible {
transform: translateY(-6px);
background: var(--on);
color: var(--on-ink);
}
.fo__dot:focus-visible {
outline: 2px solid $ft-ink;
outline-offset: 2px;
}
// ── 자동 재생 ─────────────────────────────────────────────
// 점 셋이 1/3 주기씩 어긋나 차례로 떠오르고, 그 위에서 이름 밑줄이 따로 차오른다.
// animation 단축은 delay 를 0 으로 리셋하므로 낱개 속성으로 쓴다
.fo.is-demo .fo__dot {
animation-name: foLift;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
transition: none;
}
.fo.is-demo .fo__dot--b { animation-delay: -.6667s; }
.fo.is-demo .fo__dot--c { animation-delay: -1.3333s; }
.fo.is-demo .fo__line {
animation-name: foLine;
animation-duration: $duration;
animation-timing-function: $ease-spring;
animation-iteration-count: infinite;
transition: none;
}
@keyframes foLift {
0% {
transform: translateY(0);
background: #{$ft-paper};
color: #{$ft-stone};
}
8%, 26% {
transform: translateY(-6px);
background: var(--on);
color: var(--on-ink);
}
33.33%, 100% {
transform: translateY(0);
background: #{$ft-paper};
color: #{$ft-stone};
}
}
// 복귀는 90% 안에 끝낸다 — 24프레임의 마지막 장은 95.8% 지점이다
@keyframes foLine {
0% { transform: scaleX(0); }
45%, 78% { transform: scaleX(1); }
88%, 100% { transform: scaleX(0); }
}
// 좁은 화면 — 글자와 간격만 줄이고 한 줄을 유지한다
@media (max-width: 340px), (max-height: 220px) {
.fo { gap: $sp-2; width: 100%; padding-top: $sp-2; }
.fo__mark { font-size: 14px; }
.fo__line { bottom: -3px; height: 2px; }
.fo__copy { font-size: 10px; }
.fo__social { gap: $sp-1; }
.fo__dot { width: 22px; height: 22px; }
.fo__dot svg { width: 13px; height: 13px; }
}
@media (prefers-reduced-motion: reduce) {
// .fo.is-demo .fo__dot (0,3,0) 를 이기려면 !important 가 필요하다
.fo__dot, .fo__line { animation: none !important; transition: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #fbf6ee;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.fo {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: clamp(288px, 92vw, 432px);
padding-top: 16px;
border-top: 2px solid #1b1a20;
}
.fo__mark {
position: relative;
flex: 0 0 auto;
color: #1b1a20;
font-weight: 800;
font-size: 20px;
line-height: 1.1;
letter-spacing: -0.01em;
text-decoration: none;
}
.fo__mark:focus-visible {
outline: 2px solid #479089;
outline-offset: 4px;
border-radius: 4px;
}
.fo__line {
position: absolute;
left: 0;
right: 0;
bottom: -4px;
height: 3px;
border-radius: 999px;
background: #479089;
transform: scaleX(0);
transform-origin: left center;
transition: transform 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.fo__mark:hover .fo__line,
.fo__mark:focus-visible .fo__line {
transform: scaleX(1);
}
.fo__copy {
flex: 0 1 auto;
margin: 0;
color: #575162;
font-weight: 500;
font-size: 12px;
line-height: 1.2;
}
.fo__social {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 8px;
}
.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(0.2, 1.4, 0.4, 1), background-color 220ms ease-out, color 220ms ease-out;
}
.fo__dot--b {
--on: #1b1a20;
--on-ink: #ffffff;
}
.fo__dot--c {
--on: #479089;
--on-ink: #ffffff;
}
.fo__dot:hover,
.fo__dot:focus-visible {
transform: translateY(-6px);
background: var(--on);
color: var(--on-ink);
}
.fo__dot:focus-visible {
outline: 2px solid #1b1a20;
outline-offset: 2px;
}
.fo.is-demo .fo__dot {
animation-name: foLift;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
transition: none;
}
.fo.is-demo .fo__dot--b {
animation-delay: -0.6667s;
}
.fo.is-demo .fo__dot--c {
animation-delay: -1.3333s;
}
.fo.is-demo .fo__line {
animation-name: foLine;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
transition: none;
}
@keyframes foLift {
0% {
transform: translateY(0);
background: #ffffff;
color: #575162;
}
8%, 26% {
transform: translateY(-6px);
background: var(--on);
color: var(--on-ink);
}
33.33%, 100% {
transform: translateY(0);
background: #ffffff;
color: #575162;
}
}
@keyframes foLine {
0% {
transform: scaleX(0);
}
45%, 78% {
transform: scaleX(1);
}
88%, 100% {
transform: scaleX(0);
}
}
@media (max-width: 340px), (max-height: 220px) {
.fo {
gap: 8px;
width: 100%;
padding-top: 8px;
}
.fo__mark {
font-size: 14px;
}
.fo__line {
bottom: -3px;
height: 2px;
}
.fo__copy {
font-size: 10px;
}
.fo__social {
gap: 4px;
}
.fo__dot {
width: 22px;
height: 22px;
}
.fo__dot svg {
width: 13px;
height: 13px;
}
}
@media (prefers-reduced-motion: reduce) {
.fo__dot, .fo__line {
animation: none !important;
transition: none !important;
}
}(function () {
var root = document.querySelector('.fo');
var dots = [].slice.call(root.querySelectorAll('.fo__dot'));
function stopLoop() { root.classList.remove('is-demo'); }
// 소셜 점 셋은 좌우 화살표로 옮겨 다닌다 (Home·End 는 양 끝)
dots.forEach(function (dot, i) {
dot.addEventListener('keydown', function (e) {
var to = -1;
if (e.key === 'ArrowRight') { to = (i + 1) % dots.length; }
if (e.key === 'ArrowLeft') { to = (i - 1 + dots.length) % dots.length; }
if (e.key === 'Home') { to = 0; }
if (e.key === 'End') { to = dots.length - 1; }
if (to < 0) { return; }
e.preventDefault();
stopLoop();
dots[to].focus({ preventScroll: true });
});
});
root.addEventListener('pointerenter', stopLoop);
root.addEventListener('focusin', stopLoop);
})();import { useRef, useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./FooterOneLine.module.scss";
/* ambient 에 KeyboardEvent 가 없어 파일 안에 필요한 만큼만 선언한다 */
type KeyEvt = { key: string; preventDefault(): void };
export type SocialDot = {
/** 스크린 리더가 읽을 이름 */
label: string;
icon: ReactNode;
};
export type FooterOneLineProps = {
mark?: string;
copy?: string;
navLabel?: string;
dots?: SocialDot[];
/** 점 세 개가 차례로 떠오르는 한 바퀴(ms) */
duration?: number;
easing?: string;
/** 무대 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const PhotoIcon = (): ReactNode => (
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true" focusable="false">
<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>
);
const PostIcon = (): ReactNode => (
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true" focusable="false">
<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>
);
const CodeIcon = (): ReactNode => (
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true" focusable="false">
<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>
);
const DEFAULT_DOTS: SocialDot[] = [
{ label: "사진 계정", icon: <PhotoIcon /> },
{ label: "짧은 소식 계정", icon: <PostIcon /> },
{ label: "코드 저장소", icon: <CodeIcon /> },
];
export default function FooterOneLine({
mark = "스튜디오",
copy = "© 2026 스튜디오",
navLabel = "소셜 링크",
dots = DEFAULT_DOTS,
duration = 2000,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#fbf6ee",
}: FooterOneLineProps): ReactNode {
const [loop, setLoop] = useState<boolean>(true);
const refs = useRef<Array<HTMLAnchorElement | null>>([]);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const tone = [styles.dotA, styles.dotB, styles.dotC];
/* 소셜 점 셋은 좌우 화살표로 옮겨 다닌다 (Home·End 는 양 끝) */
const onKey = (e: KeyEvt, i: number): void => {
const list = refs.current;
if (!list) { return; }
let to = -1;
if (e.key === "ArrowRight") { to = (i + 1) % list.length; }
if (e.key === "ArrowLeft") { to = (i - 1 + list.length) % list.length; }
if (e.key === "Home") { to = 0; }
if (e.key === "End") { to = list.length - 1; }
if (to < 0) { return; }
e.preventDefault();
setLoop(false);
const next = list[to];
if (next) { next.focus({ preventScroll: true }); }
};
return (
<footer
className={loop ? `${styles.fo} ${styles.isDemo}` : styles.fo}
style={vars}
role="contentinfo"
onPointerEnter={() => setLoop(false)}
onFocus={() => setLoop(false)}
>
<a className={styles.mark} href="#">
<span className={styles.markT}>{mark}</span>
<span className={styles.line} aria-hidden="true" />
</a>
<p className={styles.copy}>{copy}</p>
<nav className={styles.social} aria-label={navLabel}>
{dots.map((d: SocialDot, i: number) => (
<a
className={`${styles.dot} ${tone[i % tone.length]}`}
href="#"
key={d.label}
aria-label={d.label}
ref={(el: HTMLAnchorElement | null) => { if (refs.current) { refs.current[i] = el; } }}
onKeyDown={(e: KeyEvt) => onKey(e, i)}
>
{d.icon}
</a>
))}
</nav>
</footer>
);
}03 ミニマル一行フッター — 計測 累積面積 1.865% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · フッター デザイン コピペで終わるパターン9選
このデモが属する記事
フッター デザイン コピペで終わるパターン9選フッター デザイン コピペで探しているのは、ページの一番下でサイト全体を締めくくる区画のひな形です。ここではリンクの一覧で終わらせない九つのフッターを、そのまま貼れるHTMLとSCSSで載せました。サイトマップから波の境目まで、すべて画面の この記事のデモをすべて見る


