02記事横の縦シェアバー
記事の左に縦に付くバーのボタンが順番に横へラベルを展開します。展開は幅を伸ばすのではなく隠していた面を現す方式なので滑らかです。
使いどころマガジン型ブログ・ニュースサイトの本文横
clip-path: inset()position: sticky시차 등장

<div class="stage">
<div class="sr">
<div class="sr__view">
<div class="sr__doc">
<nav class="sr__rail" aria-label="シェアバー">
<a class="sr__link" href="#">
<span class="sr__ic"><svg width="20" height="20" 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></span>
<span class="sr__name">リンクをコピー</span>
</a>
<a class="sr__link" href="#">
<span class="sr__ic"><svg width="20" height="20" 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></span>
<span class="sr__name">メールを送る</span>
</a>
<a class="sr__link" href="#">
<span class="sr__ic"><svg width="20" height="20" 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></span>
<span class="sr__name">メッセージ</span>
</a>
<a class="sr__link" href="#">
<span class="sr__ic"><svg width="20" height="20" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M208,200a32,32,0,1,1-32-32A32,32,0,0,1,208,200ZM176,88a32,32,0,1,0-32-32A32,32,0,0,0,176,88Z" opacity="0.2"/><path d="M176,160a39.89,39.89,0,0,0-28.62,12.09l-46.1-29.63a39.8,39.8,0,0,0,0-28.92l46.1-29.63a40,40,0,1,0-8.66-13.45l-46.1,29.63a40,40,0,1,0,0,55.82l46.1,29.63A40,40,0,1,0,176,160Zm0-128a24,24,0,1,1-24,24A24,24,0,0,1,176,32ZM64,152a24,24,0,1,1,24-24A24,24,0,0,1,64,152Zm112,72a24,24,0,1,1,24-24A24,24,0,0,1,176,224Z"/></svg></span>
<span class="sr__name">もっと見る</span>
</a>
</nav>
<article class="sr__body">
<h2 class="sr__title">長い記事は横からシェアする</h2>
<span class="sr__row sr__row--full" aria-hidden="true"></span>
<span class="sr__row sr__row--wide" aria-hidden="true"></span>
<span class="sr__row sr__row--mid" aria-hidden="true"></span>
<span class="sr__row sr__row--full" aria-hidden="true"></span>
<span class="sr__row sr__row--wide" aria-hidden="true"></span>
<span class="sr__row sr__row--short" aria-hidden="true"></span>
<span class="sr__row sr__row--full" aria-hidden="true"></span>
<span class="sr__row sr__row--mid" aria-hidden="true"></span>
</article>
</div>
</div>
</div>
</div>@use "../../../../site/static/demo-tokens" as *;
// 편 277 공유 팔레트 (01 과 같은 값)
$sh-ink: #15131a;
$sh-ink2: #26232e;
$sh-paper: #ffffff;
$sh-cream: #fdf8f0;
$sh-slate: #5b5566;
$sh-mist: #bdb7c6; // 잉크 위 보조 글자 9.43:1
$sh-sky: #c8dce4; // 잉크 위 강조 11.09:1
$sh-deep: #477690;
$duration: $dur-loop; // 넷이 차례로 라벨을 펼치는 한 바퀴
$easing: $ease-spring;
$color: $sh-sky; // 펼쳐진 라벨 면 색
@include stage($sh-ink);
.sr {
box-sizing: border-box;
width: clamp(288px, 88vw, 400px);
height: 216px;
overflow: hidden;
border-radius: $r-card;
background: $sh-ink2;
box-shadow: $shadow-raised;
}
.sr__view {
height: 100%;
overflow-y: auto;
overscroll-behavior: contain;
}
// 화면보다 긴 문서 — 이 높이 차이가 sticky 가 걸릴 여유다.
// 여유를 padding-bottom 으로 주면 sticky 유효 범위(부모 콘텐츠 박스) 밖이라 안 먹는다
.sr__doc {
display: grid;
// 펼친 라벨이 본문 글자를 덮지 않게 첫 칸을 펼친 폭(136px)으로 미리 잡는다 —
// 접힌 폭(44px)으로 잡으면 펼치는 순간 라벨이 제목 위에 얹힌다 (첫 렌더 실측)
grid-template-columns: 136px 1fr;
gap: $sp-3;
min-height: 300px;
padding: $sp-3;
}
.sr__rail {
position: sticky;
top: 0;
align-self: start;
display: flex;
flex-direction: column;
gap: $sp-2;
}
.sr__link {
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
gap: $sp-2;
// 펼친 폭(136px)만큼 상자를 미리 잡아 두고 오른쪽 100px 을 잘라 둔다 —
// 라벨 첫 글자는 여백 12px + 아이콘 20px + 간격 8px 인 40px 에서 시작하는데
// 남는 36px 이 그보다 안쪽이라 접힌 상태에 라벨 글자가 안 비친다
// (첫 렌더 실측: 92px 로 자르니 남는 44px 이 40px 을 넘어 첫 글자가 비쳤다)
// 펼침은 폭을 늘리는 게 아니라 가려 둔 면을 드러내는 것이라 레이아웃이 다시 계산되지 않는다
width: 136px;
height: 44px;
padding: 0 $sp-3;
border-radius: $r-pill;
background: $sh-ink;
color: $sh-mist;
text-decoration: none;
clip-path: inset(0 100px 0 0 round $r-pill);
transition: clip-path $dur-base $easing, background-color $dur-base $easing, color $dur-base $easing;
}
.sr__ic {
flex: 0 0 auto;
display: grid;
place-items: center;
width: 20px;
}
.sr__ic svg { display: block; }
.sr__name {
white-space: nowrap;
font-weight: 600; font-size: 12px; line-height: 1;
font-family: inherit;
}
// 손으로 쓰는 경로 — .is-demo 를 뗀 뒤에도 라벨이 펼쳐진다
.sr__link:hover,
.sr__link:focus-visible {
clip-path: inset(0 0 0 0 round $r-pill);
background: $color;
color: $sh-ink; // 하늘색 면 위 잉크 글자 11.09:1
}
.sr__link:focus-visible {
outline: 2px solid $sh-paper;
outline-offset: 2px;
}
.sr__body {
display: flex;
flex-direction: column;
gap: $sp-2;
}
.sr__title {
margin: 0;
color: $sh-paper;
font-weight: 700; font-size: 14px; line-height: 1.3;
font-family: inherit;
}
.sr__row {
display: block;
height: 8px;
border-radius: $r-pill;
background: #3b3746;
}
.sr__row--full { width: 100%; }
.sr__row--wide { width: 84%; }
.sr__row--mid { width: 64%; }
.sr__row--short { width: 42%; }
// ── 자동 재생 ───────────────────────────────────────────────
// 넷이 0.16초씩 밀려 펼쳐진다. animation 단축은 delay 를 0 으로 리셋하므로 낱개 속성으로,
// 지연 구간에 접힌 기본 상태가 번쩍이지 않게 fill-mode: backwards 를 같이 준다
.is-demo .sr__link {
animation-name: railUnfold;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.is-demo .sr__link:nth-child(1) { animation-delay: 0s; }
.is-demo .sr__link:nth-child(2) { animation-delay: .16s; }
.is-demo .sr__link:nth-child(3) { animation-delay: .32s; }
.is-demo .sr__link:nth-child(4) { animation-delay: .48s; }
// 펼침 10% · 접힘 16% 의 비대칭. 복귀는 90% 안에 끝난다 — 24프레임의 마지막 장이 95.8% 지점이다.
// clip-path 는 이징으로 보간되는 속성이라 steps 와 섞지 않는다
@keyframes railUnfold {
0% { clip-path: inset(0 100px 0 0 round $r-pill); background-color: $sh-ink; color: $sh-mist; }
10%, 46% { clip-path: inset(0 0 0 0 round $r-pill); background-color: $color; color: $sh-ink; }
62%, 100% { clip-path: inset(0 100px 0 0 round $r-pill); background-color: $sh-ink; color: $sh-mist; }
}
@media (max-width: 340px), (max-height: 220px) {
.sr { height: 156px; }
.sr__doc { grid-template-columns: 124px 1fr; gap: $sp-2; min-height: 216px; padding: $sp-2; }
.sr__rail { gap: $sp-1; }
.sr__link { width: 124px; height: 40px; padding: 0 $sp-2; clip-path: inset(0 92px 0 0 round $r-pill); }
.sr__title { font-size: 13px; }
.sr__row { height: 6px; }
.sr__ic svg { width: 18px; height: 18px; }
}
@media (prefers-reduced-motion: reduce) {
// .is-demo 쪽 선택자가 (0,2,0) 이라 !important 없이는 진다
.is-demo .sr__link { animation: none !important; }
.sr__link { transition: none !important; }
// 움직임을 끊어도 호버·초점의 펼침은 그대로 남는다
.sr__link:hover, .sr__link:focus-visible { clip-path: inset(0 0 0 0 round $r-pill); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #15131a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.sr {
box-sizing: border-box;
width: clamp(288px, 88vw, 400px);
height: 216px;
overflow: hidden;
border-radius: 16px;
background: #26232e;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.sr__view {
height: 100%;
overflow-y: auto;
overscroll-behavior: contain;
}
.sr__doc {
display: grid;
grid-template-columns: 136px 1fr;
gap: 12px;
min-height: 300px;
padding: 12px;
}
.sr__rail {
position: sticky;
top: 0;
align-self: start;
display: flex;
flex-direction: column;
gap: 8px;
}
.sr__link {
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
gap: 8px;
width: 136px;
height: 44px;
padding: 0 12px;
border-radius: 999px;
background: #15131a;
color: #bdb7c6;
text-decoration: none;
clip-path: inset(0 100px 0 0 round 999px);
transition: clip-path 300ms cubic-bezier(0.2, 0.8, 0.2, 1), background-color 300ms cubic-bezier(0.2, 0.8, 0.2, 1), color 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.sr__ic {
flex: 0 0 auto;
display: grid;
place-items: center;
width: 20px;
}
.sr__ic svg {
display: block;
}
.sr__name {
white-space: nowrap;
font-weight: 600;
font-size: 12px;
line-height: 1;
font-family: inherit;
}
.sr__link:hover,
.sr__link:focus-visible {
clip-path: inset(0 0 0 0 round 999px);
background: #c8dce4;
color: #15131a;
}
.sr__link:focus-visible {
outline: 2px solid #ffffff;
outline-offset: 2px;
}
.sr__body {
display: flex;
flex-direction: column;
gap: 8px;
}
.sr__title {
margin: 0;
color: #ffffff;
font-weight: 700;
font-size: 14px;
line-height: 1.3;
font-family: inherit;
}
.sr__row {
display: block;
height: 8px;
border-radius: 999px;
background: #3b3746;
}
.sr__row--full {
width: 100%;
}
.sr__row--wide {
width: 84%;
}
.sr__row--mid {
width: 64%;
}
.sr__row--short {
width: 42%;
}
.is-demo .sr__link {
animation-name: railUnfold;
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 .sr__link:nth-child(1) {
animation-delay: 0s;
}
.is-demo .sr__link:nth-child(2) {
animation-delay: 0.16s;
}
.is-demo .sr__link:nth-child(3) {
animation-delay: 0.32s;
}
.is-demo .sr__link:nth-child(4) {
animation-delay: 0.48s;
}
@keyframes railUnfold {
0% {
clip-path: inset(0 100px 0 0 round 999px);
background-color: #15131a;
color: #bdb7c6;
}
10%, 46% {
clip-path: inset(0 0 0 0 round 999px);
background-color: #c8dce4;
color: #15131a;
}
62%, 100% {
clip-path: inset(0 100px 0 0 round 999px);
background-color: #15131a;
color: #bdb7c6;
}
}
@media (max-width: 340px), (max-height: 220px) {
.sr {
height: 156px;
}
.sr__doc {
grid-template-columns: 124px 1fr;
gap: 8px;
min-height: 216px;
padding: 8px;
}
.sr__rail {
gap: 4px;
}
.sr__link {
width: 124px;
height: 40px;
padding: 0 8px;
clip-path: inset(0 92px 0 0 round 999px);
}
.sr__title {
font-size: 13px;
}
.sr__row {
height: 6px;
}
.sr__ic svg {
width: 18px;
height: 18px;
}
}
@media (prefers-reduced-motion: reduce) {
.is-demo .sr__link {
animation: none !important;
}
.sr__link {
transition: none !important;
}
.sr__link:hover, .sr__link:focus-visible {
clip-path: inset(0 0 0 0 round 999px);
}
}(function () {
var body = document.body;
function hand() { body.classList.remove('is-demo'); } // 사람이 손을 대면 자동 재생을 끈다
document.querySelectorAll('.sr__link').forEach(function (a) {
a.addEventListener('pointerdown', hand);
a.addEventListener('focusin', hand);
a.addEventListener('click', function (e) { e.preventDefault(); hand(); });
});
document.querySelector('.sr__view').addEventListener('scroll', hand);
})();import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./ShareSideRail.module.scss";
/* ambient 에 React 의 이벤트 제네릭이 없어 필요한 것만 로컬로 선언한다 */
type ClickEvt = { preventDefault(): void };
export type ShareSideRailProps = {
/** 바에 서는 공유 방법 이름 */
labels?: string[];
/** 넷이 차례로 라벨을 펼치는 한 바퀴(ms) */
duration?: number;
easing?: string;
/** 펼쳐진 라벨 면 색 */
color?: string;
autoPlay?: boolean;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
/* Phosphor Duotone (MIT) — 사슬 · 봉투 · 말풍선 · 공유 */
const ICONS: ReactNode[] = [
<svg width="20" height="20" 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="20" height="20" 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="20" height="20" 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="20" height="20" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M208,200a32,32,0,1,1-32-32A32,32,0,0,1,208,200ZM176,88a32,32,0,1,0-32-32A32,32,0,0,0,176,88Z" opacity="0.2"/><path d="M176,160a39.89,39.89,0,0,0-28.62,12.09l-46.1-29.63a39.8,39.8,0,0,0,0-28.92l46.1-29.63a40,40,0,1,0-8.66-13.45l-46.1,29.63a40,40,0,1,0,0,55.82l46.1,29.63A40,40,0,1,0,176,160Zm0-128a24,24,0,1,1-24,24A24,24,0,0,1,176,32ZM64,152a24,24,0,1,1,24-24A24,24,0,0,1,64,152Zm112,72a24,24,0,1,1,24-24A24,24,0,0,1,176,224Z"/></svg>,
];
const ROWS = ["rowFull", "rowWide", "rowMid", "rowFull", "rowWide", "rowShort", "rowFull", "rowMid"];
export default function ShareSideRail({
labels = ["링크 복사", "메일 보내기", "메시지", "더 보기"],
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#7fd4f5",
autoPlay = true,
}: ShareSideRailProps) {
const [demo, setDemo] = useState(autoPlay);
/* 사람이 손을 대면 자동 재생을 끈다 */
function stopDemo() {
setDemo(false);
}
const vars: Vars = { "--duration": duration + "ms", "--easing": easing, "--color": color };
return (
<div className={styles.stage + (demo ? " " + styles.isDemo : "")} style={vars}>
<div className={styles.wrap}>
<div className={styles.view} onScroll={stopDemo}>
<div className={styles.doc}>
<nav className={styles.rail} aria-label="공유 바">
{labels.map((name, i) => (
<a
key={name}
className={styles.link}
href="#"
onClick={(e: ClickEvt) => {
e.preventDefault();
stopDemo();
}}
onPointerDown={stopDemo}
onFocus={stopDemo}
>
<span className={styles.ic}>{ICONS[i]}</span>
<span className={styles.name}>{name}</span>
</a>
))}
</nav>
<article className={styles.body}>
<h2 className={styles.title}>긴 글은 옆에서 공유한다</h2>
{ROWS.map((cls, i) => (
<span key={i} className={styles.row + " " + styles[cls]} aria-hidden="true" />
))}
</article>
</div>
</div>
</div>
</div>
);
}02 記事横の縦シェアバー — 計測 累積面積 15.565% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · シェアボタン デザイン パターン9選 — コピペで動く
このデモが属する記事
シェアボタン デザイン パターン9選 — コピペで動くシェアボタン デザインは、訪問者がワンタップで記事を外へ送り出す最後の扉の形そのものです。ここに集めた9つは見かけだけの作り物ではなく、押すと本当にコピーされるシェアUI を HTML・CSS・JavaScript で組んだものなので、その この記事のデモをすべて見る


