07下部固定シェアバー
画面下に付いたバーを上へスワイプするとアイコン行が展開します。行の高さは固定値ではなく、グリッドのトラックが折りたたまれた状態から開く方式で伸びます。
使いどころモバイルブログ・ニュースの読み流れの維持
grid-template-rowsaria-expandedtouch-action: pan-y

<div class="stage">
<div class="sb">
<div class="sb__view">
<div class="sb__doc">
<div class="sb__body">
<p class="sb__title">読みながらシェアする</p>
<span class="sb__row sb__row--full" aria-hidden="true"></span>
<span class="sb__row sb__row--wide" aria-hidden="true"></span>
<span class="sb__row sb__row--mid" aria-hidden="true"></span>
<span class="sb__row sb__row--full" aria-hidden="true"></span>
<span class="sb__row sb__row--short" aria-hidden="true"></span>
</div>
<div class="sb__bar">
<button class="sb__handle" type="button" aria-expanded="false" aria-controls="sb-panel">
<span class="sb__handleName">シェア方法</span>
<span class="sb__caret"><svg width="16" height="16" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M208,160H48l80-80Z" opacity="0.2"/><path d="M213.66,154.34l-80-80a8,8,0,0,0-11.32,0l-80,80A8,8,0,0,0,48,168H208a8,8,0,0,0,5.66-13.66ZM67.31,152,128,91.31,188.69,152Z"/></svg></span>
</button>
<div class="sb__panel" id="sb-panel">
<div class="sb__inner">
<a class="sb__pick" href="#" aria-label="リンクをコピー"><svg width="18" height="18" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M218.34,119.6,183.6,154.34a46.58,46.58,0,0,1-44.31,12.26c-.31.34-.62.67-.95,1L103.6,202.34A46.63,46.63,0,1,1,37.66,136.4L72.4,101.66A46.6,46.6,0,0,1,116.71,89.4c.31-.34.62-.67,1-1L152.4,53.66a46.63,46.63,0,0,1,65.94,65.94Z" opacity="0.2"/><path d="M240,88.23a54.43,54.43,0,0,1-16,37L189.25,160a54.27,54.27,0,0,1-38.63,16h-.05A54.63,54.63,0,0,1,96,119.84a8,8,0,0,1,16,.45A38.62,38.62,0,0,0,150.58,160h0a38.39,38.39,0,0,0,27.31-11.31l34.75-34.75a38.63,38.63,0,0,0-54.63-54.63l-11,11A8,8,0,0,1,135.7,59l11-11A54.65,54.65,0,0,1,224,48,54.86,54.86,0,0,1,240,88.23ZM109,185.66l-11,11A38.41,38.41,0,0,1,70.6,208h0a38.63,38.63,0,0,1-27.29-65.94L78,107.31A38.63,38.63,0,0,1,144,135.71a8,8,0,0,0,7.78,8.22H152a8,8,0,0,0,8-7.78A54.86,54.86,0,0,0,144,96a54.65,54.65,0,0,0-77.27,0L32,130.75A54.62,54.62,0,0,0,70.56,224h0a54.28,54.28,0,0,0,38.64-16l11-11A8,8,0,0,0,109,185.66Z"/></svg></a>
<a class="sb__pick" href="#" aria-label="メールで送る"><svg width="18" height="18" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M224,56l-96,88L32,56Z" opacity="0.2"/><path d="M224,48H32a8,8,0,0,0-8,8V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A8,8,0,0,0,224,48ZM203.43,64,128,133.15,52.57,64ZM216,192H40V74.19l82.59,75.71a8,8,0,0,0,10.82,0L216,74.19V192Z"/></svg></a>
<a class="sb__pick" href="#" aria-label="メッセージで送る"><svg width="18" height="18" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M224,128A96,96,0,0,1,79.93,211.11h0L42.54,223.58a8,8,0,0,1-10.12-10.12l12.47-37.39h0A96,96,0,1,1,224,128Z" opacity="0.2"/><path d="M128,24A104,104,0,0,0,36.18,176.88L24.83,210.93a16,16,0,0,0,20.24,20.24l34.05-11.35A104,104,0,1,0,128,24Zm0,192a87.87,87.87,0,0,1-44.06-11.81,8,8,0,0,0-4-1.08,7.85,7.85,0,0,0-2.53.42L40,216,52.47,178.6a8,8,0,0,0-.66-6.54A88,88,0,1,1,128,216Zm12-88a12,12,0,1,1-12-12A12,12,0,0,1,140,128Zm-44,0a12,12,0,1,1-12-12A12,12,0,0,1,96,128Zm88,0a12,12,0,1,1-12-12A12,12,0,0,1,184,128Z"/></svg></a>
<a class="sb__pick" href="#" aria-label="QRコードを表示"><svg width="18" height="18" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M112,56v48a8,8,0,0,1-8,8H56a8,8,0,0,1-8-8V56a8,8,0,0,1,8-8h48A8,8,0,0,1,112,56Zm-8,88H56a8,8,0,0,0-8,8v48a8,8,0,0,0,8,8h48a8,8,0,0,0,8-8V152A8,8,0,0,0,104,144Zm96-96H152a8,8,0,0,0-8,8v48a8,8,0,0,0,8,8h48a8,8,0,0,0,8-8V56A8,8,0,0,0,200,48Z" opacity="0.2"/><path d="M104,40H56A16,16,0,0,0,40,56v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V56A16,16,0,0,0,104,40Zm0,64H56V56h48v48Zm0,32H56a16,16,0,0,0-16,16v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V152A16,16,0,0,0,104,136Zm0,64H56V152h48v48ZM200,40H152a16,16,0,0,0-16,16v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40Zm0,64H152V56h48v48Zm-64,72V144a8,8,0,0,1,16,0v32a8,8,0,0,1-16,0Zm80-16a8,8,0,0,1-8,8H184v40a8,8,0,0,1-8,8H144a8,8,0,0,1,0-16h24V144a8,8,0,0,1,16,0v8h24A8,8,0,0,1,216,160Zm0,32v16a8,8,0,0,1-16,0V192a8,8,0,0,1,16,0Z"/></svg></a>
<a class="sb__pick" href="#" aria-label="新しいウィンドウで開く"><svg width="18" height="18" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M184,80V208a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V80a8,8,0,0,1,8-8H176A8,8,0,0,1,184,80Z" opacity="0.2"/><path d="M224,104a8,8,0,0,1-16,0V59.32l-66.33,66.34a8,8,0,0,1-11.32-11.32L196.68,48H152a8,8,0,0,1,0-16h64a8,8,0,0,1,8,8Zm-40,24a8,8,0,0,0-8,8v72H48V80h72a8,8,0,0,0,0-16H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V136A8,8,0,0,0,184,128Z"/></svg></a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>@use "../../../../site/static/demo-tokens" as *;
// 편 277 공유 팔레트 (01 과 같은 값)
$sh-ink: #15131a;
$sh-paper: #ffffff;
$sh-cream: #fdf8f0;
$sh-slate: #5b5566;
$sh-mist: #bdb7c6;
$sh-deep: #477690;
$sh-sky: #c8dce4; // 잉크 위 강조 11.09:1
$duration: $dur-loop; // 펼침 → 머무름 한 바퀴
$easing: $ease-spring;
$color: $sh-sky; // 호버·초점의 아이콘 색
@include stage($stage-yellow);
.sb {
box-sizing: border-box;
width: clamp(248px, 78vw, 320px);
height: 216px;
overflow: hidden;
border-radius: $r-card;
background: $sh-cream;
box-shadow: $shadow-raised;
}
.sb__view {
height: 100%;
overflow-y: auto;
overscroll-behavior: contain;
touch-action: pan-y; // 세로 스크롤은 브라우저에 맡기고 손잡이만 직접 읽는다
}
// 화면보다 긴 문서 — 이 높이 차이가 sticky 가 걸릴 여유다.
// 여유를 padding-bottom 으로 주면 sticky 유효 범위(부모 콘텐츠 박스) 밖이라 안 먹는다
.sb__doc {
display: flex;
flex-direction: column;
min-height: 300px;
}
.sb__body {
flex: 1 0 auto;
display: flex;
flex-direction: column;
gap: $sp-2;
padding: $sp-4;
}
.sb__title {
margin: 0;
color: $sh-ink;
font-weight: 700; font-size: 14px; line-height: 1.2;
font-family: inherit;
}
.sb__row {
display: block;
height: 8px;
border-radius: $r-pill;
background: #e3ddd2;
}
.sb__row--full { width: 100%; }
.sb__row--wide { width: 86%; }
.sb__row--mid { width: 62%; }
.sb__row--short { width: 40%; }
.sb__bar {
position: sticky;
bottom: 0;
margin-top: auto;
background: $sh-ink;
}
// 스크롤 0 에서는 sticky 가 안 걸리는 경우가 있어(260-09 실측)
// 자동 재생 동안에는 화면(.sb) 기준 절대배치로 세워 둔다
.is-demo .sb__bar {
position: absolute;
left: 0;
right: 0;
bottom: 0;
}
.is-demo .sb { position: relative; }
.sb__handle {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 36px;
padding: 0 $sp-3;
border: 0;
background: transparent;
color: $sh-mist; // 잉크 위 보조 글자 9.43:1
cursor: pointer;
touch-action: pan-y;
}
.sb__handle:focus-visible {
outline: 2px solid $sh-paper;
outline-offset: -2px;
}
.sb__handleName {
font-weight: 700; font-size: 12px; line-height: 1;
font-family: inherit;
}
.sb__caret {
display: grid;
place-items: center;
transform: rotate(0deg);
transition: transform $dur-base $easing;
}
.sb__caret svg { display: block; }
// 접힌 행의 높이는 고정 px 이 아니라 격자 트랙이다 —
// height: auto 는 중간값이 계산되지 않지만 0fr 과 1fr 사이는 계산된다
.sb__panel {
display: grid;
grid-template-rows: 0fr;
overflow: hidden;
transition: grid-template-rows $dur-base $easing;
}
.sb__inner {
display: flex;
justify-content: space-between;
gap: $sp-2;
min-height: 0; // 0fr 에서 실제로 접히게 하는 한 줄
padding: 0 $sp-3 $sp-3;
}
.sb__pick {
box-sizing: border-box;
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: $r-control;
background: #2b2833;
color: $sh-paper;
text-decoration: none;
}
.sb__pick svg { display: block; }
.sb__pick:hover, .sb__pick:focus-visible { color: $color; }
.sb__pick:focus-visible {
outline: 2px solid $sh-paper;
outline-offset: 2px;
}
// 손으로 쓰는 경로 — .is-demo 를 뗀 뒤에도 펼침 상태가 그대로 보인다
.sb.is-open .sb__panel { grid-template-rows: 1fr; }
.sb.is-open .sb__caret { transform: rotate(180deg); }
// ── 자동 재생 ───────────────────────────────────────────────
// 행은 루프 안에서 다시 접히지 않는다 — 대표 이미지는 앞 장과 가장 크게 달라진 장이라
// 접힘을 재현하면 아이콘 없는 장이 뽑힌다. 되돌림은 루프 경계에서만 일어난다
// 행은 한 프레임에 열린다(steps) — 트랙이 서서히 자라면 아이콘이 위쪽만 보이는 장이
// 가장 크게 달라진 장이 되어 대표 이미지가 반쯤 잘린 아이콘 줄이 된다 (첫 렌더 실측).
// 실제 손으로 쓸 때는 위의 transition 이 그대로라 부드럽게 열린다
.is-demo .sb__panel {
animation-name: sbOpen;
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.is-demo .sb__caret {
animation-name: sbTurn;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
// 아이콘 다섯이 행보다 늦게 떠올라 프레임을 채운다. 시차에는 fill-mode: backwards 를 같이 준다
.is-demo .sb__pick {
animation-name: sbPop;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.is-demo .sb__pick:nth-child(1) { animation-delay: .50s; }
.is-demo .sb__pick:nth-child(2) { animation-delay: .58s; }
.is-demo .sb__pick:nth-child(3) { animation-delay: .66s; }
.is-demo .sb__pick:nth-child(4) { animation-delay: .74s; }
.is-demo .sb__pick:nth-child(5) { animation-delay: .82s; }
@keyframes sbOpen {
0%, 24.9% { grid-template-rows: 0fr; }
25%, 100% { grid-template-rows: 1fr; }
}
@keyframes sbTurn {
0%, 8% { transform: rotate(0deg); }
25%, 100% { transform: rotate(180deg); }
}
@keyframes sbPop {
0% { transform: translateY(8px) scale(.88); }
26%, 100% { transform: translateY(0) scale(1); }
}
@media (max-width: 340px), (max-height: 220px) {
.sb { height: 156px; }
.sb__doc { min-height: 220px; }
.sb__body { gap: $sp-1; padding: $sp-3; }
.sb__title { font-size: 13px; }
.sb__row { height: 6px; }
.sb__handle { height: 32px; padding: 0 $sp-2; }
.sb__inner { gap: $sp-1; padding: 0 $sp-2 $sp-2; }
.sb__pick { width: 32px; height: 32px; }
.sb__pick svg { width: 16px; height: 16px; }
}
@media (prefers-reduced-motion: reduce) {
// .is-demo 쪽 선택자가 (0,2,0) 이라 !important 없이는 진다
.is-demo .sb__panel, .is-demo .sb__caret, .is-demo .sb__pick { animation: none !important; }
.sb__panel, .sb__caret { transition: none !important; }
// 움직임을 끊어도 펼친 행과 아이콘은 그대로 남는다
.sb.is-open .sb__panel { grid-template-rows: 1fr; }
.sb__pick { transform: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.sb {
box-sizing: border-box;
width: clamp(248px, 78vw, 320px);
height: 216px;
overflow: hidden;
border-radius: 16px;
background: #fdf8f0;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.sb__view {
height: 100%;
overflow-y: auto;
overscroll-behavior: contain;
touch-action: pan-y;
}
.sb__doc {
display: flex;
flex-direction: column;
min-height: 300px;
}
.sb__body {
flex: 1 0 auto;
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
}
.sb__title {
margin: 0;
color: #15131a;
font-weight: 700;
font-size: 14px;
line-height: 1.2;
font-family: inherit;
}
.sb__row {
display: block;
height: 8px;
border-radius: 999px;
background: #e3ddd2;
}
.sb__row--full {
width: 100%;
}
.sb__row--wide {
width: 86%;
}
.sb__row--mid {
width: 62%;
}
.sb__row--short {
width: 40%;
}
.sb__bar {
position: sticky;
bottom: 0;
margin-top: auto;
background: #15131a;
}
.is-demo .sb__bar {
position: absolute;
left: 0;
right: 0;
bottom: 0;
}
.is-demo .sb {
position: relative;
}
.sb__handle {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 36px;
padding: 0 12px;
border: 0;
background: transparent;
color: #bdb7c6;
cursor: pointer;
touch-action: pan-y;
}
.sb__handle:focus-visible {
outline: 2px solid #ffffff;
outline-offset: -2px;
}
.sb__handleName {
font-weight: 700;
font-size: 12px;
line-height: 1;
font-family: inherit;
}
.sb__caret {
display: grid;
place-items: center;
transform: rotate(0deg);
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.sb__caret svg {
display: block;
}
.sb__panel {
display: grid;
grid-template-rows: 0fr;
overflow: hidden;
transition: grid-template-rows 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.sb__inner {
display: flex;
justify-content: space-between;
gap: 8px;
min-height: 0;
padding: 0 12px 12px;
}
.sb__pick {
box-sizing: border-box;
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: 12px;
background: #2b2833;
color: #ffffff;
text-decoration: none;
}
.sb__pick svg {
display: block;
}
.sb__pick:hover, .sb__pick:focus-visible {
color: #c8dce4;
}
.sb__pick:focus-visible {
outline: 2px solid #ffffff;
outline-offset: 2px;
}
.sb.is-open .sb__panel {
grid-template-rows: 1fr;
}
.sb.is-open .sb__caret {
transform: rotate(180deg);
}
.is-demo .sb__panel {
animation-name: sbOpen;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.is-demo .sb__caret {
animation-name: sbTurn;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.is-demo .sb__pick {
animation-name: sbPop;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.is-demo .sb__pick:nth-child(1) {
animation-delay: 0.5s;
}
.is-demo .sb__pick:nth-child(2) {
animation-delay: 0.58s;
}
.is-demo .sb__pick:nth-child(3) {
animation-delay: 0.66s;
}
.is-demo .sb__pick:nth-child(4) {
animation-delay: 0.74s;
}
.is-demo .sb__pick:nth-child(5) {
animation-delay: 0.82s;
}
@keyframes sbOpen {
0%, 24.9% {
grid-template-rows: 0fr;
}
25%, 100% {
grid-template-rows: 1fr;
}
}
@keyframes sbTurn {
0%, 8% {
transform: rotate(0deg);
}
25%, 100% {
transform: rotate(180deg);
}
}
@keyframes sbPop {
0% {
transform: translateY(8px) scale(0.88);
}
26%, 100% {
transform: translateY(0) scale(1);
}
}
@media (max-width: 340px), (max-height: 220px) {
.sb {
height: 156px;
}
.sb__doc {
min-height: 220px;
}
.sb__body {
gap: 4px;
padding: 12px;
}
.sb__title {
font-size: 13px;
}
.sb__row {
height: 6px;
}
.sb__handle {
height: 32px;
padding: 0 8px;
}
.sb__inner {
gap: 4px;
padding: 0 8px 8px;
}
.sb__pick {
width: 32px;
height: 32px;
}
.sb__pick svg {
width: 16px;
height: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
.is-demo .sb__panel, .is-demo .sb__caret, .is-demo .sb__pick {
animation: none !important;
}
.sb__panel, .sb__caret {
transition: none !important;
}
.sb.is-open .sb__panel {
grid-template-rows: 1fr;
}
.sb__pick {
transform: none;
}
}(function () {
var body = document.body;
var root = document.querySelector('.sb');
var handle = document.querySelector('.sb__handle');
var picks = Array.prototype.slice.call(document.querySelectorAll('.sb__pick'));
var startY = 0;
function hand() { body.classList.remove('is-demo'); } // 사람이 손을 대면 자동 재생을 끈다
function setOpen(on) {
root.classList.toggle('is-open', on);
handle.setAttribute('aria-expanded', on ? 'true' : 'false');
picks.forEach(function (a) { a.tabIndex = on ? 0 : -1; }); // 접힌 행은 Tab 순서에서 뺀다
}
handle.addEventListener('click', function () {
hand();
setOpen(handle.getAttribute('aria-expanded') !== 'true');
});
handle.addEventListener('focusin', hand);
// 위로 쓸어 올리면 펼쳐지고 아래로 내리면 접힌다 — 세로 스크롤은 그대로 둔다
handle.addEventListener('pointerdown', function (e) { hand(); startY = e.clientY; });
handle.addEventListener('pointerup', function (e) {
var dy = startY - e.clientY;
if (dy > 12) setOpen(true);
else if (dy < -12) setOpen(false);
});
setOpen(false);
})();import { useRef, useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./ShareStickyBar.module.scss";
/* ambient 에 React 의 이벤트 제네릭이 없어 필요한 것만 로컬로 선언한다 */
type ClickEvt = { preventDefault(): void };
type PointerEvt = { clientY: number };
export type ShareStickyBarProps = {
/** 펼쳐지는 아이콘의 접근성 이름 */
labels?: string[];
/** 펼침 → 머무름 한 바퀴(ms) */
duration?: number;
easing?: string;
/** 호버·초점의 아이콘 색 */
color?: string;
autoPlay?: boolean;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
/* Phosphor Duotone (MIT) — 사슬 · 봉투 · 말풍선 · QR · 새 창 */
const ICONS: ReactNode[] = [
<svg width="18" height="18" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M218.34,119.6,183.6,154.34a46.58,46.58,0,0,1-44.31,12.26c-.31.34-.62.67-.95,1L103.6,202.34A46.63,46.63,0,1,1,37.66,136.4L72.4,101.66A46.6,46.6,0,0,1,116.71,89.4c.31-.34.62-.67,1-1L152.4,53.66a46.63,46.63,0,0,1,65.94,65.94Z" opacity="0.2"/><path d="M240,88.23a54.43,54.43,0,0,1-16,37L189.25,160a54.27,54.27,0,0,1-38.63,16h-.05A54.63,54.63,0,0,1,96,119.84a8,8,0,0,1,16,.45A38.62,38.62,0,0,0,150.58,160h0a38.39,38.39,0,0,0,27.31-11.31l34.75-34.75a38.63,38.63,0,0,0-54.63-54.63l-11,11A8,8,0,0,1,135.7,59l11-11A54.65,54.65,0,0,1,224,48,54.86,54.86,0,0,1,240,88.23ZM109,185.66l-11,11A38.41,38.41,0,0,1,70.6,208h0a38.63,38.63,0,0,1-27.29-65.94L78,107.31A38.63,38.63,0,0,1,144,135.71a8,8,0,0,0,7.78,8.22H152a8,8,0,0,0,8-7.78A54.86,54.86,0,0,0,144,96a54.65,54.65,0,0,0-77.27,0L32,130.75A54.62,54.62,0,0,0,70.56,224h0a54.28,54.28,0,0,0,38.64-16l11-11A8,8,0,0,0,109,185.66Z"/></svg>,
<svg width="18" height="18" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M224,56l-96,88L32,56Z" opacity="0.2"/><path d="M224,48H32a8,8,0,0,0-8,8V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A8,8,0,0,0,224,48ZM203.43,64,128,133.15,52.57,64ZM216,192H40V74.19l82.59,75.71a8,8,0,0,0,10.82,0L216,74.19V192Z"/></svg>,
<svg width="18" height="18" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M224,128A96,96,0,0,1,79.93,211.11h0L42.54,223.58a8,8,0,0,1-10.12-10.12l12.47-37.39h0A96,96,0,1,1,224,128Z" opacity="0.2"/><path d="M128,24A104,104,0,0,0,36.18,176.88L24.83,210.93a16,16,0,0,0,20.24,20.24l34.05-11.35A104,104,0,1,0,128,24Zm0,192a87.87,87.87,0,0,1-44.06-11.81,8,8,0,0,0-4-1.08,7.85,7.85,0,0,0-2.53.42L40,216,52.47,178.6a8,8,0,0,0-.66-6.54A88,88,0,1,1,128,216Zm12-88a12,12,0,1,1-12-12A12,12,0,0,1,140,128Zm-44,0a12,12,0,1,1-12-12A12,12,0,0,1,96,128Zm88,0a12,12,0,1,1-12-12A12,12,0,0,1,184,128Z"/></svg>,
<svg width="18" height="18" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M112,56v48a8,8,0,0,1-8,8H56a8,8,0,0,1-8-8V56a8,8,0,0,1,8-8h48A8,8,0,0,1,112,56Zm-8,88H56a8,8,0,0,0-8,8v48a8,8,0,0,0,8,8h48a8,8,0,0,0,8-8V152A8,8,0,0,0,104,144Zm96-96H152a8,8,0,0,0-8,8v48a8,8,0,0,0,8,8h48a8,8,0,0,0,8-8V56A8,8,0,0,0,200,48Z" opacity="0.2"/><path d="M104,40H56A16,16,0,0,0,40,56v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V56A16,16,0,0,0,104,40Zm0,64H56V56h48v48Zm0,32H56a16,16,0,0,0-16,16v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V152A16,16,0,0,0,104,136Zm0,64H56V152h48v48ZM200,40H152a16,16,0,0,0-16,16v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40Zm0,64H152V56h48v48Zm-64,72V144a8,8,0,0,1,16,0v32a8,8,0,0,1-16,0Zm80-16a8,8,0,0,1-8,8H184v40a8,8,0,0,1-8,8H144a8,8,0,0,1,0-16h24V144a8,8,0,0,1,16,0v8h24A8,8,0,0,1,216,160Zm0,32v16a8,8,0,0,1-16,0V192a8,8,0,0,1,16,0Z"/></svg>,
<svg width="18" height="18" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M184,80V208a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V80a8,8,0,0,1,8-8H176A8,8,0,0,1,184,80Z" opacity="0.2"/><path d="M224,104a8,8,0,0,1-16,0V59.32l-66.33,66.34a8,8,0,0,1-11.32-11.32L196.68,48H152a8,8,0,0,1,0-16h64a8,8,0,0,1,8,8Zm-40,24a8,8,0,0,0-8,8v72H48V80h72a8,8,0,0,0,0-16H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V136A8,8,0,0,0,184,128Z"/></svg>,
];
const ROWS = ["rowFull", "rowWide", "rowMid", "rowFull", "rowShort"];
export default function ShareStickyBar({
labels = ["링크 복사", "메일로 보내기", "메시지로 보내기", "QR 코드 보기", "새 창으로 열기"],
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#7fd4f5",
autoPlay = true,
}: ShareStickyBarProps) {
const [demo, setDemo] = useState(autoPlay);
const [open, setOpen] = useState(false);
const startRef = useRef<number>(0) as { current: number };
/* 사람이 손을 대면 자동 재생을 끈다 */
function stopDemo() {
setDemo(false);
}
function applyToggle() {
stopDemo();
setOpen((prev) => !prev);
}
/* 위로 쓸어 올리면 펼쳐지고 아래로 내리면 접힌다 — 세로 스크롤은 그대로 둔다 */
function applyDown(e: PointerEvt) {
stopDemo();
startRef.current = e.clientY;
}
function applyUp(e: PointerEvt) {
const dy = startRef.current - e.clientY;
if (dy > 12) setOpen(true);
else if (dy < -12) setOpen(false);
}
const vars: Vars = { "--duration": duration + "ms", "--easing": easing, "--color": color };
return (
<div className={styles.stage + (demo ? " " + styles.isDemo : "")} style={vars}>
<div className={styles.sticky + (open ? " " + styles.isOpen : "")}>
<div className={styles.view}>
<div className={styles.doc}>
<div className={styles.body}>
<p className={styles.title}>읽는 중에도 공유한다</p>
{ROWS.map((cls, i) => (
<span key={i} className={styles.row + " " + styles[cls]} aria-hidden="true" />
))}
</div>
<div className={styles.bar}>
<button
className={styles.handle}
type="button"
aria-expanded={open}
onClick={applyToggle}
onPointerDown={applyDown}
onPointerUp={applyUp}
onFocus={stopDemo}
>
<span className={styles.handleName}>공유 방법</span>
<span className={styles.caret}><svg width="16" height="16" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M208,160H48l80-80Z" opacity="0.2"/><path d="M213.66,154.34l-80-80a8,8,0,0,0-11.32,0l-80,80A8,8,0,0,0,48,168H208a8,8,0,0,0,5.66-13.66ZM67.31,152,128,91.31,188.69,152Z"/></svg></span>
</button>
<div className={styles.panel}>
<div className={styles.inner}>
{labels.map((name, i) => (
<a
key={name}
className={styles.pick}
href="#"
aria-label={name}
tabIndex={open ? 0 : -1}
onClick={(e: ClickEvt) => {
e.preventDefault();
stopDemo();
}}
>
{ICONS[i]}
</a>
))}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}07 下部固定シェアバー — 計測 累積面積 14.452% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · シェアボタン デザイン パターン9選 — コピペで動く
このデモが属する記事
シェアボタン デザイン パターン9選 — コピペで動くシェアボタン デザインは、訪問者がワンタップで記事を外へ送り出す最後の扉の形そのものです。ここに集めた9つは見かけだけの作り物ではなく、押すと本当にコピーされるシェアUI を HTML・CSS・JavaScript で組んだものなので、その この記事のデモをすべて見る


