06Card liên hệ báo chí
Dưới tên và chức danh phụ trách media, bấm nút email là địa chỉ được copy vào clipboard thật, icon dấu tích hiện trên nút một lúc rồi nhãn gốc trở lại.
Dùng choCard phụ trách truyền thông nhận hỏi tin bài và yêu cầu phỏng vấn
clipboardstroke-dashoffsetclick

<main class="stage">
<section class="mc is-demo">
<span class="mc__orb mc__orb--1" aria-hidden="true"></span>
<span class="mc__orb mc__orb--2" aria-hidden="true"></span>
<span class="mc__avatar" aria-hidden="true"></span>
<p class="mc__name">Kim Press</p>
<p class="mc__role">Trưởng nhóm PR & Media</p>
<button class="mc__copy" type="button" data-email="[email protected]">
<span class="mc__copy-label">[email protected]</span>
<svg class="mc__copy-ic" viewBox="0 0 20 20" aria-hidden="true"><polyline points="4,10 8,14 16,5"/></svg>
</button>
<p class="mc__hint" role="status" aria-live="polite"></p>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 복사 버튼 전환
$easing: $ease-pop;
$color: $subject-blue; // 버튼·체크 색
@include stage($stage-yellow);
.mc {
position: relative;
box-sizing: border-box; // width 에 패딩을 포함 — 아니면 320px 에서 패딩만큼 넘친다(실측 2026-09-10)
width: min(352px, 92vw); // 320 + 좌우패딩(sp-4×2=32) — border-box 전환 전 데스크톱 박스 크기를 그대로 유지(92vw 쪽은 그대로라 모바일엔 영향 없음)
text-align: center;
overflow: hidden;
isolation: isolate;
border-radius: $r-card;
padding: $sp-5 $sp-4 $sp-4;
}
.mc__orb { position: absolute; border-radius: 50%; filter: blur(16px); z-index: -1; }
.mc__orb--1 { width: 150px; height: 150px; left: -40px; top: -30px; background: radial-gradient(circle, $subject-blue, transparent 68%); opacity: .5; animation: mc-drift 8s ease-in-out infinite; }
.mc__orb--2 { width: 130px; height: 130px; right: -30px; bottom: -40px; background: radial-gradient(circle, $subject-ink, transparent 68%); opacity: .32; animation: mc-drift 7s ease-in-out infinite reverse; }
@keyframes mc-drift {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate($sp-6, -$sp-5) scale(1.25); }
}
.mc__avatar {
display: block;
width: 48px; height: 48px;
margin: 0 auto $sp-2;
border-radius: 50%;
background: $subject-ink;
}
.mc__name { margin: 0 0 $sp-1; font-weight: 800; font-size: clamp(14px, 4vw, 16px); line-height: 1.2; font-family: inherit; color: $subject-ink; }
.mc__role { margin: 0 0 $sp-4; font-weight: 500; font-size: clamp(10px, 2.8vw, 11px); line-height: 1.4; font-family: inherit; color: rgba($subject-ink, .72); }
.mc__copy {
display: inline-flex; align-items: center; gap: $sp-1;
padding: $sp-2 $sp-4;
border-radius: $r-control;
border: 1px solid rgba(23, 20, 26, .16);
background: none;
color: $subject-ink;
font-weight: 700; font-size: 11px; line-height: 1; font-family: inherit;
cursor: pointer;
max-width: 100%;
transition: border-color $duration $easing, background $duration $easing;
}
.mc__copy-ic {
width: 12px; height: 12px; flex: 0 0 auto;
fill: none;
stroke: $color;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 20;
stroke-dashoffset: 20;
opacity: 0;
transition: stroke-dashoffset $duration $easing, opacity $duration $easing;
}
.mc__copy.is-copied { border-color: $color; background: rgba(47, 109, 246, .08); }
.mc__copy.is-copied .mc__copy-ic { stroke-dashoffset: 0; opacity: 1; }
.mc__hint { min-height: 14px; margin: $sp-2 0 0; font-weight: 600; font-size: 10px; line-height: 1.4; font-family: inherit; color: $color; }
// ── 데모 루프: 복사 버튼을 클릭하기 전까지 체크가 옅게 나타났다 사라진다(항상 재생) ──────
.mc.is-demo .mc__copy-ic { animation: mc-hint 2s $easing infinite; }
@keyframes mc-hint {
0%, 60% { stroke-dashoffset: 20; opacity: 0; }
78%, 90% { stroke-dashoffset: 0; opacity: .5; }
100% { stroke-dashoffset: 20; opacity: 0; }
}
// ── 반응형(BD9) ──────────────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.mc { padding: $sp-3 $sp-3; }
.mc__avatar { width: 36px; height: 36px; margin-bottom: $sp-1; }
.mc__role { margin-bottom: $sp-2; }
.mc__copy { padding: $sp-1 $sp-3; font-size: 10px; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.mc__orb { animation: none !important; }
.mc__copy, .mc__copy-ic { transition: none; }
.mc.is-demo .mc__copy-ic { animation: none !important; }
}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);
}
.mc {
position: relative;
box-sizing: border-box;
width: min(352px, 92vw);
text-align: center;
overflow: hidden;
isolation: isolate;
border-radius: 16px;
padding: 20px 16px 16px;
}
.mc__orb {
position: absolute;
border-radius: 50%;
filter: blur(16px);
z-index: -1;
}
.mc__orb--1 {
width: 150px;
height: 150px;
left: -40px;
top: -30px;
background: radial-gradient(circle, #2f6df6, transparent 68%);
opacity: 0.5;
animation: mc-drift 8s ease-in-out infinite;
}
.mc__orb--2 {
width: 130px;
height: 130px;
right: -30px;
bottom: -40px;
background: radial-gradient(circle, #17141a, transparent 68%);
opacity: 0.32;
animation: mc-drift 7s ease-in-out infinite reverse;
}
@keyframes mc-drift {
0%, 100% {
transform: translate(0, 0) scale(1);
}
50% {
transform: translate(24px, -20px) scale(1.25);
}
}
.mc__avatar {
display: block;
width: 48px;
height: 48px;
margin: 0 auto 8px;
border-radius: 50%;
background: #17141a;
}
.mc__name {
margin: 0 0 4px;
font-weight: 800;
font-size: clamp(14px, 4vw, 16px);
line-height: 1.2;
font-family: inherit;
color: #17141a;
}
.mc__role {
margin: 0 0 16px;
font-weight: 500;
font-size: clamp(10px, 2.8vw, 11px);
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
}
.mc__copy {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 8px 16px;
border-radius: 12px;
border: 1px solid rgba(23, 20, 26, 0.16);
background: none;
color: #17141a;
font-weight: 700;
font-size: 11px;
line-height: 1;
font-family: inherit;
cursor: pointer;
max-width: 100%;
transition: border-color 300ms cubic-bezier(0.2, 1.4, 0.4, 1), background 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.mc__copy-ic {
width: 12px;
height: 12px;
flex: 0 0 auto;
fill: none;
stroke: #2f6df6;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 20;
stroke-dashoffset: 20;
opacity: 0;
transition: stroke-dashoffset 300ms cubic-bezier(0.2, 1.4, 0.4, 1), opacity 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.mc__copy.is-copied {
border-color: #2f6df6;
background: rgba(47, 109, 246, 0.08);
}
.mc__copy.is-copied .mc__copy-ic {
stroke-dashoffset: 0;
opacity: 1;
}
.mc__hint {
min-height: 14px;
margin: 8px 0 0;
font-weight: 600;
font-size: 10px;
line-height: 1.4;
font-family: inherit;
color: #2f6df6;
}
.mc.is-demo .mc__copy-ic {
animation: mc-hint 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes mc-hint {
0%, 60% {
stroke-dashoffset: 20;
opacity: 0;
}
78%, 90% {
stroke-dashoffset: 0;
opacity: 0.5;
}
100% {
stroke-dashoffset: 20;
opacity: 0;
}
}
@media (max-width: 340px), (max-height: 220px) {
.mc {
padding: 12px 12px;
}
.mc__avatar {
width: 36px;
height: 36px;
margin-bottom: 4px;
}
.mc__role {
margin-bottom: 8px;
}
.mc__copy {
padding: 4px 12px;
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.mc__orb {
animation: none !important;
}
.mc__copy, .mc__copy-ic {
transition: none;
}
.mc.is-demo .mc__copy-ic {
animation: none !important;
}
}// 실제 동작: 버튼을 누르면 이메일이 실제로 클립보드에 복사되고, 잠깐 체크 아이콘으로 바뀐 뒤 되돌아간다.
var root = document.querySelector('.mc');
var btn = document.querySelector('.mc__copy');
var hint = document.querySelector('.mc__hint');
var revertTimer = null;
btn.addEventListener('click', function () {
root.classList.remove('is-demo');
var email = btn.getAttribute('data-email');
var showCopied = function () {
btn.classList.add('is-copied');
hint.textContent = 'Đã copy địa chỉ email.';
clearTimeout(revertTimer);
revertTimer = setTimeout(function () { btn.classList.remove('is-copied'); }, 1600);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(email).then(showCopied, showCopied);
} else {
var t = document.createElement('textarea');
t.value = email; document.body.appendChild(t); t.select();
try { document.execCommand('copy'); } catch (e) {}
document.body.removeChild(t);
showCopied();
}
});import { useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./PressContact.module.scss";
export type PressContactProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const EMAIL = "[email protected]";
export default function PressContact({ duration = 300, easing = "cubic-bezier(.2,1.4,.4,1)", color = "#2f6df6" }: PressContactProps) {
const [copied, setCopied] = useState(false);
const [hint, setHint] = useState("");
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
async function onCopy() {
try {
await navigator.clipboard.writeText(EMAIL);
} catch {
// 클립보드 권한이 없어도 상태는 그대로 갱신한다(폴백은 vanilla 판 참고)
}
setCopied(true);
setHint("이메일 주소를 복사했습니다.");
if (timer.current) clearTimeout(timer.current);
timer.current = setTimeout(() => setCopied(false), 1600);
}
return (
<section className={styles.mc} style={vars}>
<span className={`${styles.orb} ${styles.orb1}`} aria-hidden="true" />
<span className={`${styles.orb} ${styles.orb2}`} aria-hidden="true" />
<span className={styles.avatar} aria-hidden="true" />
<p className={styles.name}>김프레스</p>
<p className={styles.role}>PR · 미디어 담당</p>
<button type="button" className={copied ? `${styles.copy} ${styles.copied}` : styles.copy} onClick={onCopy}>
<span className={styles.copyLabel}>{EMAIL}</span>
<svg className={styles.copyIc} viewBox="0 0 20 20" aria-hidden="true"><polyline points="4,10 8,14 16,5" /></svg>
</button>
<p className={styles.hint} role="status" aria-live="polite">{hint}</p>
</section>
);
}06 Card liên hệ báo chí — Diện tích đo được 3.774% · khung hình chuyển động 6/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 mẫu press page design — dán vào landing
Bài viết chứa demo này
9 mẫu press page design — dán vào landingPress page design là bộ code gom sẵn từng khối của trang báo chí: press release, media kit, liên hệ người phụ trách. Từ Xem tất cả demo trong bài này


