09Email and embed share box
The email tab opens a pre-filled message with subject and link; the embed tab offers a read-only code field and a copy button. The tab underline travels by cell width.
Best forRe-sharing service write-ups, partner distribution material
mailto:readonlytranslateX

<div class="stage">
<section class="ee" aria-label="Share by mail or embed">
<div class="ee__tabs" role="tablist" aria-label="Share options">
<button class="ee__tab is-on" type="button" role="tab" id="ee-t1" aria-controls="ee-p1" aria-selected="true">Mail</button>
<button class="ee__tab" type="button" role="tab" id="ee-t2" aria-controls="ee-p2" aria-selected="false" tabindex="-1">Embed</button>
<span class="ee__line" aria-hidden="true"></span>
</div>
<div class="ee__panels">
<div class="ee__panel ee__panel--mail" role="tabpanel" id="ee-p1" aria-labelledby="ee-t1">
<p class="ee__label">The message your reader gets</p>
<a class="ee__mail" href="mailto:?subject=%EA%B3%B5%EC%9C%A0%ED%95%A9%EB%8B%88%EB%8B%A4&body=https%3A%2F%2Fexample.com%2Fposts%2Fshare-ui">
<span class="ee__mailIc"><svg width="18" height="18" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M223.69,42.18l-58.22,192a8,8,0,0,1-14.92,1.25L108,148,20.58,105.45a8,8,0,0,1,1.25-14.92l192-58.22A8,8,0,0,1,223.69,42.18Z" opacity="0.2"/><path d="M227.32,28.68a16,16,0,0,0-15.66-4.08l-.15,0L19.57,82.84a16,16,0,0,0-2.49,29.8L102,154l41.3,84.87A15.86,15.86,0,0,0,157.74,248q.69,0,1.38-.06a15.88,15.88,0,0,0,14-11.51l58.2-191.94c0-.05,0-.1,0-.15A16,16,0,0,0,227.32,28.68ZM157.83,231.85l-.05.14,0-.07-40.06-82.3,48-48a8,8,0,0,0-11.31-11.31l-48,48L24.08,98.25l-.07,0,.14,0L216,40Z"/></svg></span>
<span class="ee__mailName">Open in your mail app</span>
</a>
</div>
<div class="ee__panel ee__panel--embed" role="tabpanel" id="ee-p2" aria-labelledby="ee-t2" hidden>
<p class="ee__label">Code to paste as is</p>
<div class="ee__field">
<input class="ee__code" type="text" value="<iframe src="https://example.com/embed"></iframe>" readonly aria-label="Code to paste">
<button class="ee__copy" type="button" aria-label="Copy code"><svg width="16" height="16" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M216,40V168H168V88H88V40Z" opacity="0.2"/><path d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"/></svg></button>
</div>
</div>
</div>
</section>
</div>@use "../../../../site/static/demo-tokens" as *;
// 편 277 공유 팔레트 (01 과 같은 값)
$sh-ink: #15131a;
$sh-paper: #ffffff;
$sh-cream: #fdf8f0;
$sh-slate: #5b5566;
$sh-deep: #477690; // 흰 면 위 강조 6.94:1
$duration: $dur-loop; // 두 탭을 오가는 한 바퀴
$easing: $ease-spring;
$color: $sh-deep; // 밑줄과 강조 색
@include stage($stage-orange);
.ee {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: $sp-3;
width: clamp(280px, 86vw, 384px);
padding: $sp-4;
border-radius: $r-card;
background: $sh-paper;
box-shadow: $shadow-raised;
}
// 칸을 격자로 나눈다 — 밑줄이 짐작한 px 이 아니라 100% 단위로 움직인다 (BD19)
.ee__tabs {
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
border-bottom: 1px solid #ddd6ca;
}
.ee__tab {
box-sizing: border-box;
height: 36px;
padding: 0 $sp-2;
border: 0;
background: transparent;
color: $sh-slate; // 흰 면 위 보조 글자 7.15:1
cursor: pointer;
font-weight: 700; font-size: 13px; line-height: 1;
font-family: inherit;
}
.ee__tab.is-on { color: $sh-ink; }
.ee__tab:focus-visible {
outline: 2px solid $color;
outline-offset: -2px;
}
.ee__line {
position: absolute;
left: 0;
bottom: -1px;
width: 50%; // 칸 하나 = 100% → translateX(100%) 이 정확히 한 칸
height: 2px;
border-radius: $r-pill;
background: $color;
transform: translateX(calc(var(--n, 0) * 100%));
transition: transform $dur-base $easing;
}
.ee__panels { display: grid; }
.ee__panel {
display: flex;
flex-direction: column;
gap: $sp-2;
}
// 클래스에 display 를 주면 [hidden] 의 기본 display:none 을 이겨 두 판이 함께 보인다 —
// 숨김 규칙을 다시 적어 줘야 한다 (정적 상태 스냅샷 실측)
.ee__panel[hidden] { display: none; }
.ee__label {
margin: 0;
color: $sh-slate;
font-weight: 600; font-size: 12px; line-height: 1;
font-family: inherit;
}
.ee__mail {
box-sizing: border-box;
align-self: flex-start;
display: flex;
align-items: center;
gap: $sp-2;
height: 40px;
padding: 0 $sp-4;
border-radius: $r-control;
background: $sh-ink;
color: $sh-paper; // 잉크 면 위 흰 글자 18.42:1
text-decoration: none;
font-weight: 700; font-size: 12px; line-height: 1;
font-family: inherit;
}
.ee__mail:focus-visible {
outline: 2px solid $sh-ink;
outline-offset: 2px;
}
.ee__mailIc { display: grid; place-items: center; }
.ee__mailIc svg { display: block; }
.ee__field {
display: flex;
gap: $sp-2;
}
.ee__code {
flex: 1 1 auto;
box-sizing: border-box;
min-width: 0;
height: 40px;
padding: 0 $sp-3;
border: 1px solid #ddd6ca;
border-radius: $r-control;
background: $sh-cream;
color: $sh-ink;
font-weight: 500; font-size: 11px; line-height: 1;
font-family: inherit;
}
.ee__code:focus-visible {
outline: 2px solid $color;
outline-offset: 1px;
}
.ee__copy {
flex: 0 0 auto;
box-sizing: border-box;
display: grid;
place-items: center;
width: 40px;
height: 40px;
padding: 0;
border: 0;
border-radius: $r-control;
background: $sh-ink;
color: $sh-paper;
cursor: pointer;
}
.ee__copy svg { display: block; }
.ee__copy:focus-visible {
outline: 2px solid $sh-ink;
outline-offset: 2px;
}
// ── 자동 재생 ───────────────────────────────────────────────
// 미리보기 동안에는 두 판을 같은 격자 칸에 포개고 한 프레임에 컷으로 갈아 끼운다.
// 겹쳐 두는 쪽이 판을 바꿔도 상자 높이가 안 흔들린다
.is-demo .ee__panel { grid-area: 1 / 1; }
.is-demo .ee__panel[hidden] { display: flex; }
.is-demo .ee__line {
animation-name: eeSlide;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
// 판과 라벨 교체는 steps(1, end) 컷 — 두 판이 겹친 반투명 유령 장이 안 생긴다
.is-demo .ee__panel, .is-demo .ee__tab {
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.is-demo .ee__panel--mail { animation-name: eeShowA; }
.is-demo .ee__panel--embed { animation-name: eeShowB; }
.is-demo .ee__tab:nth-child(1) { animation-name: eeOnA; }
.is-demo .ee__tab:nth-child(2) { animation-name: eeOnB; }
// 컷만 있으면 움직인 프레임이 두 장뿐이다 — 컷 옆에 투명도가 아닌 transform 움직임을 붙인다.
// 시차에는 fill-mode: backwards 를 같이 준다
.is-demo .ee__mail, .is-demo .ee__field {
animation-name: eePop;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.is-demo .ee__field { animation-delay: 1s; }
@keyframes eeSlide {
0%, 4% { transform: translateX(0); }
46%, 50% { transform: translateX(100%); }
92%, 100% { transform: translateX(0); }
}
// 컷 시각은 24프레임 눈금(4.1667% 간격)에 맞춘다 — 밑줄이 그 칸에 닿은 뒤에 판을 바꾼다
@keyframes eeShowA { 0%, 49.9% { visibility: visible; } 50%, 95.7% { visibility: hidden; } 95.8%, 100% { visibility: visible; } }
@keyframes eeShowB { 0%, 49.9% { visibility: hidden; } 50%, 95.7% { visibility: visible; } 95.8%, 100% { visibility: hidden; } }
@keyframes eeOnA { 0%, 49.9% { color: $sh-ink; } 50%, 95.7% { color: $sh-slate; } 95.8%, 100% { color: $sh-ink; } }
@keyframes eeOnB { 0%, 49.9% { color: $sh-slate; } 50%, 95.7% { color: $sh-ink; } 95.8%, 100% { color: $sh-slate; } }
@keyframes eePop {
0% { transform: translateY(10px) scale(.96); }
14%, 50% { transform: translateY(0) scale(1); }
50.1%, 100% { transform: translateY(0) scale(1); }
}
@media (max-width: 340px), (max-height: 220px) {
.ee { gap: $sp-2; padding: $sp-3; }
.ee__tab { height: 32px; font-size: 12px; }
.ee__mail { height: 36px; padding: 0 $sp-3; font-size: 11px; }
.ee__code { height: 36px; padding: 0 $sp-2; font-size: 10px; }
.ee__copy { width: 36px; height: 36px; }
}
@media (prefers-reduced-motion: reduce) {
// .is-demo 쪽 선택자가 (0,2,0) 이라 !important 없이는 진다
.is-demo .ee__line, .is-demo .ee__panel, .is-demo .ee__tab,
.is-demo .ee__mail, .is-demo .ee__field { animation: none !important; }
.ee__line { transition: none !important; }
// 움직임을 끊어도 고른 탭과 그 판은 그대로 보인다
.is-demo .ee__panel[hidden] { display: none; }
}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);
}
.ee {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 12px;
width: clamp(280px, 86vw, 384px);
padding: 16px;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.ee__tabs {
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
border-bottom: 1px solid #ddd6ca;
}
.ee__tab {
box-sizing: border-box;
height: 36px;
padding: 0 8px;
border: 0;
background: transparent;
color: #5b5566;
cursor: pointer;
font-weight: 700;
font-size: 13px;
line-height: 1;
font-family: inherit;
}
.ee__tab.is-on {
color: #15131a;
}
.ee__tab:focus-visible {
outline: 2px solid #477690;
outline-offset: -2px;
}
.ee__line {
position: absolute;
left: 0;
bottom: -1px;
width: 50%;
height: 2px;
border-radius: 999px;
background: #477690;
transform: translateX(calc(var(--n, 0) * 100%));
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ee__panels {
display: grid;
}
.ee__panel {
display: flex;
flex-direction: column;
gap: 8px;
}
.ee__panel[hidden] {
display: none;
}
.ee__label {
margin: 0;
color: #5b5566;
font-weight: 600;
font-size: 12px;
line-height: 1;
font-family: inherit;
}
.ee__mail {
box-sizing: border-box;
align-self: flex-start;
display: flex;
align-items: center;
gap: 8px;
height: 40px;
padding: 0 16px;
border-radius: 12px;
background: #15131a;
color: #ffffff;
text-decoration: none;
font-weight: 700;
font-size: 12px;
line-height: 1;
font-family: inherit;
}
.ee__mail:focus-visible {
outline: 2px solid #15131a;
outline-offset: 2px;
}
.ee__mailIc {
display: grid;
place-items: center;
}
.ee__mailIc svg {
display: block;
}
.ee__field {
display: flex;
gap: 8px;
}
.ee__code {
flex: 1 1 auto;
box-sizing: border-box;
min-width: 0;
height: 40px;
padding: 0 12px;
border: 1px solid #ddd6ca;
border-radius: 12px;
background: #fdf8f0;
color: #15131a;
font-weight: 500;
font-size: 11px;
line-height: 1;
font-family: inherit;
}
.ee__code:focus-visible {
outline: 2px solid #477690;
outline-offset: 1px;
}
.ee__copy {
flex: 0 0 auto;
box-sizing: border-box;
display: grid;
place-items: center;
width: 40px;
height: 40px;
padding: 0;
border: 0;
border-radius: 12px;
background: #15131a;
color: #ffffff;
cursor: pointer;
}
.ee__copy svg {
display: block;
}
.ee__copy:focus-visible {
outline: 2px solid #15131a;
outline-offset: 2px;
}
.is-demo .ee__panel {
grid-area: 1/1;
}
.is-demo .ee__panel[hidden] {
display: flex;
}
.is-demo .ee__line {
animation-name: eeSlide;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.is-demo .ee__panel, .is-demo .ee__tab {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.is-demo .ee__panel--mail {
animation-name: eeShowA;
}
.is-demo .ee__panel--embed {
animation-name: eeShowB;
}
.is-demo .ee__tab:nth-child(1) {
animation-name: eeOnA;
}
.is-demo .ee__tab:nth-child(2) {
animation-name: eeOnB;
}
.is-demo .ee__mail, .is-demo .ee__field {
animation-name: eePop;
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 .ee__field {
animation-delay: 1s;
}
@keyframes eeSlide {
0%, 4% {
transform: translateX(0);
}
46%, 50% {
transform: translateX(100%);
}
92%, 100% {
transform: translateX(0);
}
}
@keyframes eeShowA {
0%, 49.9% {
visibility: visible;
}
50%, 95.7% {
visibility: hidden;
}
95.8%, 100% {
visibility: visible;
}
}
@keyframes eeShowB {
0%, 49.9% {
visibility: hidden;
}
50%, 95.7% {
visibility: visible;
}
95.8%, 100% {
visibility: hidden;
}
}
@keyframes eeOnA {
0%, 49.9% {
color: #15131a;
}
50%, 95.7% {
color: #5b5566;
}
95.8%, 100% {
color: #15131a;
}
}
@keyframes eeOnB {
0%, 49.9% {
color: #5b5566;
}
50%, 95.7% {
color: #15131a;
}
95.8%, 100% {
color: #5b5566;
}
}
@keyframes eePop {
0% {
transform: translateY(10px) scale(0.96);
}
14%, 50% {
transform: translateY(0) scale(1);
}
50.1%, 100% {
transform: translateY(0) scale(1);
}
}
@media (max-width: 340px), (max-height: 220px) {
.ee {
gap: 8px;
padding: 12px;
}
.ee__tab {
height: 32px;
font-size: 12px;
}
.ee__mail {
height: 36px;
padding: 0 12px;
font-size: 11px;
}
.ee__code {
height: 36px;
padding: 0 8px;
font-size: 10px;
}
.ee__copy {
width: 36px;
height: 36px;
}
}
@media (prefers-reduced-motion: reduce) {
.is-demo .ee__line, .is-demo .ee__panel, .is-demo .ee__tab,
.is-demo .ee__mail, .is-demo .ee__field {
animation: none !important;
}
.ee__line {
transition: none !important;
}
.is-demo .ee__panel[hidden] {
display: none;
}
}(function () {
var body = document.body;
var root = document.querySelector('.ee');
var tabs = Array.prototype.slice.call(document.querySelectorAll('.ee__tab'));
var panes = Array.prototype.slice.call(document.querySelectorAll('.ee__panel'));
var at = 0;
function hand() { body.classList.remove('is-demo'); } // 사람이 손을 대면 자동 재생을 끈다
// 밑줄은 짐작한 px 이 아니라 칸 번호 하나로만 움직인다 — 칸 폭이 바뀌어도 안 어긋난다
function move(i, focus) {
at = (i + tabs.length) % tabs.length;
tabs.forEach(function (t, k) {
var on = k === at;
t.classList.toggle('is-on', on);
t.setAttribute('aria-selected', on ? 'true' : 'false');
t.tabIndex = on ? 0 : -1; // 로빙 탭인덱스
panes[k].hidden = !on;
});
root.style.setProperty('--n', at);
if (focus) tabs[at].focus({ preventScroll: true });
}
tabs.forEach(function (t, k) {
t.addEventListener('click', function () { hand(); move(k, true); });
});
root.querySelector('.ee__tabs').addEventListener('keydown', function (e) {
var d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
if (!d) return;
e.preventDefault();
hand();
move(at + d, true);
});
root.addEventListener('pointerdown', hand);
root.addEventListener('focusin', hand);
document.querySelector('.ee__copy').addEventListener('click', function () {
var field = document.querySelector('.ee__code');
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(field.value);
else field.select();
});
move(0, false);
})();import { useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./ShareEmailEmbed.module.scss";
/* ambient 에 React 의 이벤트 제네릭이 없어 필요한 것만 로컬로 선언한다 */
type KeyEvt = { key: string; preventDefault(): void };
type InputEl = { value: string; select(): void };
export type ShareEmailEmbedProps = {
/** 공유할 주소와 심을 코드 */
url?: string;
embedCode?: string;
/** 두 탭을 오가는 한 바퀴(ms) */
duration?: number;
easing?: string;
/** 밑줄과 강조 색 */
color?: string;
autoPlay?: boolean;
};
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
"--n"?: number;
};
const TABS = ["메일", "퍼가기"];
export default function ShareEmailEmbed({
url = "https://example.com/posts/share-ui",
embedCode = '<iframe src="https://example.com/embed"></iframe>',
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#0f5f8a",
autoPlay = true,
}: ShareEmailEmbedProps) {
const [demo, setDemo] = useState(autoPlay);
const [at, setAt] = useState(0);
const codeRef = useRef<InputEl>(null) as { current: InputEl | null };
/* 사람이 손을 대면 자동 재생을 끈다 */
function stopDemo() {
setDemo(false);
}
/* 밑줄은 짐작한 px 이 아니라 칸 번호 하나로만 움직인다 */
function applyMove(i: number) {
stopDemo();
setAt((i + TABS.length) % TABS.length);
}
function applyKey(e: KeyEvt) {
const d = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
if (!d) return;
e.preventDefault();
applyMove(at + d);
}
function applyCopy() {
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(embedCode);
else if (codeRef.current) codeRef.current.select();
}
const vars: Vars = {
"--duration": duration + "ms",
"--easing": easing,
"--color": color,
"--n": at,
};
const mailHref =
"mailto:?subject=" + encodeURIComponent("공유합니다") + "&body=" + encodeURIComponent(url);
return (
<div className={styles.stage + (demo ? " " + styles.isDemo : "")} style={vars}>
<section className={styles.embed} aria-label="메일과 퍼가기로 공유" style={vars}>
<div className={styles.tabs} role="tablist" aria-label="공유 방법" onKeyDown={applyKey}>
{TABS.map((name, i) => (
<button
key={name}
className={styles.tab + (at === i ? " " + styles.isOn : "")}
type="button"
role="tab"
id={"ee-t" + i}
aria-controls={"ee-p" + i}
aria-selected={at === i}
tabIndex={at === i ? 0 : -1}
onClick={() => applyMove(i)}
onPointerDown={stopDemo}
>
{name}
</button>
))}
<span className={styles.line} aria-hidden="true" />
</div>
<div className={styles.panels}>
<div
className={styles.panel + " " + styles.panelMail}
role="tabpanel"
id="ee-p0"
aria-labelledby="ee-t0"
hidden={at !== 0}
>
<p className={styles.label}>받는 사람에게 갈 편지</p>
<a className={styles.mail} href={mailHref}>
<span className={styles.mailIc}><svg width="18" height="18" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M223.69,42.18l-58.22,192a8,8,0,0,1-14.92,1.25L108,148,20.58,105.45a8,8,0,0,1,1.25-14.92l192-58.22A8,8,0,0,1,223.69,42.18Z" opacity="0.2"/><path d="M227.32,28.68a16,16,0,0,0-15.66-4.08l-.15,0L19.57,82.84a16,16,0,0,0-2.49,29.8L102,154l41.3,84.87A15.86,15.86,0,0,0,157.74,248q.69,0,1.38-.06a15.88,15.88,0,0,0,14-11.51l58.2-191.94c0-.05,0-.1,0-.15A16,16,0,0,0,227.32,28.68ZM157.83,231.85l-.05.14,0-.07-40.06-82.3,48-48a8,8,0,0,0-11.31-11.31l-48,48L24.08,98.25l-.07,0,.14,0L216,40Z"/></svg></span>
<span className={styles.mailName}>메일 앱으로 열기</span>
</a>
</div>
<div
className={styles.panel + " " + styles.panelEmbed}
role="tabpanel"
id="ee-p1"
aria-labelledby="ee-t1"
hidden={at !== 1}
>
<p className={styles.label}>그대로 붙이는 코드</p>
<div className={styles.field}>
<input
className={styles.code}
type="text"
defaultValue={embedCode}
readOnly
aria-label="붙여 넣을 코드"
ref={codeRef as unknown as never}
/>
<button className={styles.copy} type="button" aria-label="코드 복사" onClick={applyCopy}>
<svg width="16" height="16" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M216,40V168H168V88H88V40Z" opacity="0.2"/><path d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"/></svg>
</button>
</div>
</div>
</div>
</section>
</div>
);
}09 Email and embed share box — Measured area 13.145% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · Social Share Buttons: 9 Copy-Paste UI Patterns
The post this demo lives in
Social Share Buttons: 9 Copy-Paste UI PatternsSocial share buttons are the last door a page leaves through, and the one tap a fake demo gives away. The nine here real See all demos in this post


