04会場案内
오른쪽 지도 플레이스홀더 위 핀이 계속 파문을 그리는 동안, 왼쪽 지하철·버스·자차 탭 세 개를 누르면 그 이동수단 설명 문단만 opacity 로 바뀝니다.
使いどころ
opacityaria-selectedclick

<main class="stage">
<section class="vn is-demo">
<div class="vn__info">
<p class="vn__name">○○コンベンションセンター 3階</p>
<p class="vn__addr">ソウル江南区○○路123</p>
<div class="vn__tabs" role="tablist">
<button class="vn__tab" type="button" role="tab" aria-selected="true" data-i="0">地下鉄</button>
<button class="vn__tab" type="button" role="tab" aria-selected="false" data-i="1">バス</button>
<button class="vn__tab" type="button" role="tab" aria-selected="false" data-i="2">車</button>
<span class="vn__ind" aria-hidden="true"></span>
</div>
<div class="vn__panels">
<p class="vn__panel vn__panel--0">2号線○○駅3番出口、徒歩5分。</p>
<p class="vn__panel vn__panel--1">幹線000・支線0000番、○○駅下車。</p>
<p class="vn__panel vn__panel--2">地下駐車場2時間無料、以降1時間2,000ウォン。</p>
</div>
</div>
<div class="vn__map" aria-hidden="true">
<span class="vn__pin"></span>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 탭 인디케이터·패널 전환
$easing: $ease-spring;
$color: $subject-blue; // 인디케이터·핀 색
@include stage($stage-paper);
.vn {
display: grid;
grid-template-columns: 1.1fr 1fr;
gap: $sp-4;
width: min(420px, 94vw);
align-items: stretch;
}
@media (max-width: 340px) {
.vn { grid-template-columns: 1fr; }
.vn__map { display: none; }
.vn__name { margin-bottom: $sp-1; font-size: 12px; }
.vn__addr { margin-bottom: $sp-2; font-size: 10px; }
.vn__tabs { margin-bottom: $sp-2; }
.vn__tab { padding: $sp-1 0; font-size: 10px; }
.vn__panel { font-size: 10px; }
}
.vn__name { margin: 0 0 $sp-1; font-weight: 700; font-size: clamp(12px, 3.4vw, 13px); line-height: 1.3; font-family: inherit; color: $subject-ink; }
.vn__addr { margin: 0 0 $sp-3; font-weight: 500; font-size: 11px; line-height: 1.4; font-family: inherit; color: rgba(23, 20, 26, .6); }
.vn__tabs {
position: relative;
display: flex;
border-bottom: 1px solid rgba(23, 20, 26, .12);
margin-bottom: $sp-3;
}
.vn__tab {
flex: 1 1 0;
padding: $sp-2 0;
background: none; border: 0; cursor: pointer;
font-weight: 600; font-size: 11px; line-height: 1; font-family: inherit;
color: rgba($subject-ink, .72);
-webkit-tap-highlight-color: transparent;
}
.vn__tab[aria-selected="true"] { color: $subject-ink; }
.vn__ind {
position: absolute; left: 0; bottom: -1px;
width: calc(100% / 3); height: 2px;
background: $color;
transform: translateX(0%);
transition: transform $duration $easing;
}
.vn__panels { position: relative; display: grid; min-height: 34px; }
.vn__panel {
grid-area: 1 / 1;
margin: 0;
font-weight: 500; font-size: 11px; line-height: 1.5; font-family: inherit;
color: rgba(23, 20, 26, .75);
opacity: 0;
transition: opacity $duration $easing;
}
.vn__panel--0 { opacity: 1; }
.vn__map {
position: relative;
border-radius: $r-card;
background:
linear-gradient(rgba(23, 20, 26, .08) 1px, transparent 1px) 0 0 / 20px 20px,
linear-gradient(90deg, rgba(23, 20, 26, .08) 1px, transparent 1px) 0 0 / 20px 20px,
$subject-cream;
min-height: 120px;
}
.vn__pin {
position: absolute; left: 46%; top: 42%;
width: $sp-3; height: $sp-3;
border-radius: 50%;
background: $color;
box-shadow: 0 0 0 0 rgba(47, 109, 246, .5);
animation: vn-ping 2s ease-out infinite;
}
@keyframes vn-ping {
0% { box-shadow: 0 0 0 0 rgba(47, 109, 246, .5); }
70% { box-shadow: 0 0 0 14px rgba(47, 109, 246, 0); }
100% { box-shadow: 0 0 0 14px rgba(47, 109, 246, 0); }
}
// ── 데모 루프: 클릭 전까지 탭 세 개를 스스로 오간다 ──────────────
.vn.is-demo .vn__ind { animation: vn-loop $dur-loop $easing infinite; }
.vn.is-demo .vn__panel--0 { animation: vn-panel-0 $dur-loop $easing infinite; }
.vn.is-demo .vn__panel--1 { animation: vn-panel-1 $dur-loop $easing infinite; }
.vn.is-demo .vn__panel--2 { animation: vn-panel-2 $dur-loop $easing infinite; }
@keyframes vn-loop {
0%, 15% { transform: translateX(0%); }
33%, 48% { transform: translateX(100%); }
66%, 81% { transform: translateX(200%); }
100% { transform: translateX(0%); }
}
@keyframes vn-panel-0 { 0%, 15% { opacity: 1; } 30%, 100% { opacity: 0; } }
@keyframes vn-panel-1 { 0%, 30% { opacity: 0; } 45%, 63% { opacity: 1; } 78%, 100% { opacity: 0; } }
@keyframes vn-panel-2 { 0%, 63% { opacity: 0; } 78%, 96% { opacity: 1; } 100% { opacity: 0; } }
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.vn__ind, .vn__panel { transition: none; }
.vn__pin { animation: none; }
.vn.is-demo .vn__ind,
.vn.is-demo .vn__panel--0,
.vn.is-demo .vn__panel--1,
.vn.is-demo .vn__panel--2 { 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: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.vn {
display: grid;
grid-template-columns: 1.1fr 1fr;
gap: 16px;
width: min(420px, 94vw);
align-items: stretch;
}
@media (max-width: 340px) {
.vn {
grid-template-columns: 1fr;
}
.vn__map {
display: none;
}
.vn__name {
margin-bottom: 4px;
font-size: 12px;
}
.vn__addr {
margin-bottom: 8px;
font-size: 10px;
}
.vn__tabs {
margin-bottom: 8px;
}
.vn__tab {
padding: 4px 0;
font-size: 10px;
}
.vn__panel {
font-size: 10px;
}
}
.vn__name {
margin: 0 0 4px;
font-weight: 700;
font-size: clamp(12px, 3.4vw, 13px);
line-height: 1.3;
font-family: inherit;
color: #17141a;
}
.vn__addr {
margin: 0 0 12px;
font-weight: 500;
font-size: 11px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.6);
}
.vn__tabs {
position: relative;
display: flex;
border-bottom: 1px solid rgba(23, 20, 26, 0.12);
margin-bottom: 12px;
}
.vn__tab {
flex: 1 1 0;
padding: 8px 0;
background: none;
border: 0;
cursor: pointer;
font-weight: 600;
font-size: 11px;
line-height: 1;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
-webkit-tap-highlight-color: transparent;
}
.vn__tab[aria-selected=true] {
color: #17141a;
}
.vn__ind {
position: absolute;
left: 0;
bottom: -1px;
width: 33.3333333333%;
height: 2px;
background: #2f6df6;
transform: translateX(0%);
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.vn__panels {
position: relative;
display: grid;
min-height: 34px;
}
.vn__panel {
grid-area: 1/1;
margin: 0;
font-weight: 500;
font-size: 11px;
line-height: 1.5;
font-family: inherit;
color: rgba(23, 20, 26, 0.75);
opacity: 0;
transition: opacity 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.vn__panel--0 {
opacity: 1;
}
.vn__map {
position: relative;
border-radius: 16px;
background: linear-gradient(rgba(23, 20, 26, 0.08) 1px, transparent 1px) 0 0/20px 20px, linear-gradient(90deg, rgba(23, 20, 26, 0.08) 1px, transparent 1px) 0 0/20px 20px, #fff7e6;
min-height: 120px;
}
.vn__pin {
position: absolute;
left: 46%;
top: 42%;
width: 12px;
height: 12px;
border-radius: 50%;
background: #2f6df6;
box-shadow: 0 0 0 0 rgba(47, 109, 246, 0.5);
animation: vn-ping 2s ease-out infinite;
}
@keyframes vn-ping {
0% {
box-shadow: 0 0 0 0 rgba(47, 109, 246, 0.5);
}
70% {
box-shadow: 0 0 0 14px rgba(47, 109, 246, 0);
}
100% {
box-shadow: 0 0 0 14px rgba(47, 109, 246, 0);
}
}
.vn.is-demo .vn__ind {
animation: vn-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.vn.is-demo .vn__panel--0 {
animation: vn-panel-0 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.vn.is-demo .vn__panel--1 {
animation: vn-panel-1 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.vn.is-demo .vn__panel--2 {
animation: vn-panel-2 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes vn-loop {
0%, 15% {
transform: translateX(0%);
}
33%, 48% {
transform: translateX(100%);
}
66%, 81% {
transform: translateX(200%);
}
100% {
transform: translateX(0%);
}
}
@keyframes vn-panel-0 {
0%, 15% {
opacity: 1;
}
30%, 100% {
opacity: 0;
}
}
@keyframes vn-panel-1 {
0%, 30% {
opacity: 0;
}
45%, 63% {
opacity: 1;
}
78%, 100% {
opacity: 0;
}
}
@keyframes vn-panel-2 {
0%, 63% {
opacity: 0;
}
78%, 96% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.vn__ind, .vn__panel {
transition: none;
}
.vn__pin {
animation: none;
}
.vn.is-demo .vn__ind,
.vn.is-demo .vn__panel--0,
.vn.is-demo .vn__panel--1,
.vn.is-demo .vn__panel--2 {
animation: none !important;
}
}// 실제 동작: 탭을 클릭하면 그 이동수단 설명만 실제로 보인다.
var root = document.querySelector('.vn');
var tabs = root.querySelectorAll('.vn__tab');
tabs.forEach(function (t) {
t.addEventListener('click', function () {
root.classList.remove('is-demo');
tabs.forEach(function (o) { o.setAttribute('aria-selected', 'false'); });
t.setAttribute('aria-selected', 'true');
root.querySelector('.vn__ind').style.transform = 'translateX(' + (Number(t.dataset.i) * 100) + '%)';
root.querySelectorAll('.vn__panel').forEach(function (p, i) {
p.style.opacity = String(i === Number(t.dataset.i) ? 1 : 0);
});
});
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./VenueBlock.module.scss";
const DIRECTIONS = [
{ label: "지하철", text: "2호선 ○○역 3번 출구, 도보 5분." },
{ label: "버스", text: "간선 000·지선 0000번, ○○역 하차." },
{ label: "자차", text: "지하 주차장 2시간 무료, 이후 시간당 2천 원." },
];
export type VenueBlockProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function VenueBlock({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: VenueBlockProps) {
const [active, setActive] = useState(0);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.vn} style={vars}>
<div>
<p className={styles.name}>○○ 컨벤션센터 3층</p>
<p className={styles.addr}>서울 강남구 ○○로 123</p>
<div className={styles.tabs} role="tablist">
{DIRECTIONS.map((d, i) => (
<button
key={d.label}
type="button"
role="tab"
aria-selected={i === active}
className={styles.tab}
onClick={() => setActive(i)}
>
{d.label}
</button>
))}
<span className={styles.ind} style={{ transform: `translateX(${active * 100}%)` }} aria-hidden="true" />
</div>
<p className={styles.panel}>{DIRECTIONS[active].text}</p>
</div>
<div className={styles.map} aria-hidden="true">
<span className={styles.pin} />
</div>
</section>
);
}04 会場案内 — 計測 累積面積 2.01% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · 【コピペOK】カンファレンス LP セクション テンプレート9選
このデモが属する記事
【コピペOK】カンファレンス LP セクション テンプレート9選カンファレンス LP セクションテンプレートは、ヒーロー・スケジュール・登壇者紹介のようなイベントランディングページ用ブロックを、LPにそのまま貼れる形でまとめたコード集です。9つのうち1つは、実際の目標時刻まで秒単位で減っていくカウントダ この記事のデモをすべて見る


