06Window header that carries the file name
The header carries three dots, a file icon, and the file name, so it says where to paste before you read a single line. With more than one file the header becomes the tab strip, and only the chosen tab is aria-selected true.
Best forSetup guides that touch several files at once
aria-selectedborder-radiustranslateY

<main class="stage">
<div class="fw is-demo" id="fw">
<div class="fw__head">
<span class="fw__dots" aria-hidden="true">
<span class="fw__dot fw__dot--1"></span>
<span class="fw__dot fw__dot--2"></span>
<span class="fw__dot fw__dot--3"></span>
</span>
<span class="fw__title">
<span class="fw__icon" aria-hidden="true"><svg viewBox="0 0 256 256" width="14" height="14"><g fill="currentColor"><path d="M208 88h-56V32Z" opacity=".2"/><path d="M181.66 146.34a8 8 0 0 1 0 11.32l-24 24a8 8 0 0 1-11.32-11.32L164.69 152l-18.35-18.34a8 8 0 0 1 11.32-11.32Zm-72-24a8 8 0 0 0-11.32 0l-24 24a8 8 0 0 0 0 11.32l24 24a8 8 0 0 0 11.32-11.32L91.31 152l18.35-18.34a8 8 0 0 0 0-11.32M216 88v128a16 16 0 0 1-16 16H56a16 16 0 0 1-16-16V40a16 16 0 0 1 16-16h96a8 8 0 0 1 5.66 2.34l56 56A8 8 0 0 1 216 88m-56-8h28.69L160 51.31Zm40 136V96h-48a8 8 0 0 1-8-8V40H56v176z"/></g></svg></span>
<span class="fw__filenameSlot">
<span class="fw__filename" id="fw-title">index.html</span>
<span class="fw__filenameDemo fw__filenameDemo--1" aria-hidden="true">index.html</span>
<span class="fw__filenameDemo fw__filenameDemo--2" aria-hidden="true">style.css</span>
</span>
</span>
</div>
<div class="fw__tabs" role="tablist" aria-labelledby="fw-title">
<span class="fw__ptr" id="fw-ptr" aria-hidden="true"></span>
<button class="fw__tab fw__tab--1" id="fw-tab1" type="button" role="tab" aria-selected="true" aria-controls="fw-panel1" tabindex="0">index.html</button>
<button class="fw__tab fw__tab--2" id="fw-tab2" type="button" role="tab" aria-selected="false" aria-controls="fw-panel2" tabindex="-1">style.css</button>
</div>
<div class="fw__body">
<pre class="fw__panel fw__panel--1 is-active" id="fw-panel1" role="tabpanel" aria-labelledby="fw-tab1" aria-hidden="false"><code><span class="c"><</span><span class="k">section</span> class=<span class="s">"card"</span><span class="c">></span>
<span class="c"><</span><span class="k">h2</span><span class="c">></span>Hello<span class="c"></</span><span class="k">h2</span><span class="c">></span>
<span class="c"><</span><span class="k">p</span><span class="c">></span>Ship it.<span class="c"></</span><span class="k">p</span><span class="c">></span>
<span class="c"><</span><span class="k">button</span><span class="c">></span>Go<span class="c"></</span><span class="k">button</span><span class="c">></span>
<span class="c"></</span><span class="k">section</span><span class="c">></span></code></pre>
<pre class="fw__panel fw__panel--2" id="fw-panel2" role="tabpanel" aria-labelledby="fw-tab2" aria-hidden="true"><code><span class="k">.card</span> <span class="c">{</span>
padding: <span class="n">16px</span><span class="c">;</span>
border-radius: <span class="n">12px</span><span class="c">;</span>
box-shadow: <span class="n">0 2px 8px</span><span class="c">;</span>
<span class="c">}</span></code></pre>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-yellow);
// 구문 색 — 어두운 패널
$syn-key: $stage-orange;
$syn-str: $stage-yellow;
$syn-num: $subject-sky;
$syn-dim: rgba($subject-cream, .72);
$syn-txt: $subject-cream;
$tab-on-bg: rgba(255, 247, 230, .14);
$tab-off-bg: rgba(255, 247, 230, 0);
$tab-on-fg: $subject-cream;
$tab-off-fg: rgba(255, 247, 230, .5);
.fw {
box-sizing: border-box;
position: relative;
width: 288px;
max-width: 100%;
border-radius: $r-card;
overflow: hidden;
box-shadow: $shadow-raised;
background: $stage-ink;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.fw__head {
display: flex;
align-items: center;
gap: $sp-2;
height: 28px;
padding: 0 $sp-3;
}
.fw__dots { display: inline-flex; align-items: center; gap: $sp-1; flex: 0 0 auto; }
.fw__dot { width: 8px; height: 8px; border-radius: 50%; }
.fw__dot--1 { background: $error; }
.fw__dot--2 { background: $stage-yellow; }
.fw__dot--3 { background: $subject-mint; }
.fw__title { display: inline-flex; align-items: center; gap: $sp-1; min-width: 0; color: rgba(255, 247, 230, .7); }
.fw__icon { display: inline-flex; flex: 0 0 auto; }
.fw__icon svg { display: block; }
// 머리 파일 이름 자리 — 실제(.fw__filename, JS 가 textContent 를 바꾼다)와
// 데모 전용 두 겹(.fw__filenameDemo)을 겹쳐 둔다. 데모 루프에서는 탭 컷과 같은
// 18%/84% 에 steps(1,end) 로 교체(크로스페이드 금지), 사람이 클릭하면 is-demo 가
// 빠지고 .fw__filename 만 보이며 그 textContent 가 갈아 끼워진다.
.fw__filenameSlot { position: relative; display: inline-block; min-width: 0; }
.fw__filename,
.fw__filenameDemo {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
font-size: 11px;
font-weight: 700;
line-height: 1;
}
.fw__filenameDemo {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
}
.fw.is-demo .fw__filename { opacity: 0; }
.fw__tabs {
position: relative;
display: flex;
align-items: flex-end;
gap: $sp-1;
padding: 0 $sp-3;
background: rgba(0, 0, 0, .22);
}
.fw__ptr {
position: absolute;
left: 44px;
top: 4px;
// 다른 항목과 같은 포인터 링 — 속이 찬 파란 점은 화면의 UI 부품처럼 읽힌다 (poster 육안 실측)
width: 12px;
height: 12px;
border-radius: 50%;
background: $stage-ink;
border: 2px solid $subject-cream;
box-shadow: $shadow-press;
pointer-events: none;
opacity: 0;
transform: translate(0, 0) scale(1);
}
.fw__tab {
position: relative;
display: inline-flex;
align-items: center;
height: 24px;
padding: 0 $sp-3;
border: 0;
border-radius: $r-xs;
background: $tab-off-bg;
color: $tab-off-fg;
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
font-size: 11px;
font-weight: 700;
line-height: 1;
cursor: pointer;
transform: translateY(0);
}
.fw__tab:focus-visible { outline: 2px solid $color; outline-offset: -2px; }
.fw__tab[aria-selected="true"] {
background: $tab-on-bg;
color: $tab-on-fg;
transform: translateY(-2px);
}
.fw__body { position: relative; height: 114px; overflow: hidden; }
.fw__panel {
position: absolute;
inset: 0;
box-sizing: border-box;
margin: 0;
padding: $sp-3;
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
font-size: 11px;
line-height: 18px;
color: $syn-txt;
white-space: pre;
opacity: 0;
visibility: hidden;
}
.fw__panel.is-active { opacity: 1; visibility: visible; }
.fw__panel .k { color: $syn-key; }
.fw__panel .s { color: $syn-str; }
.fw__panel .n { color: $syn-num; }
.fw__panel .c { color: $syn-dim; }
// ── 자동 루프 (2s) ────────────────────────────────────
// 0~16% 포인터가 두 번째 탭으로 · 18% 컷: 탭 전환 + 코드 컷 교체 · 18~30% 새 탭이 붙으며 점 3개가 차례 팝
// 34~66% 포인터 복귀 · 84% 컷: 첫 탭으로 복귀 · 84~100% 정지
.fw.is-demo .fw__ptr {
opacity: 1;
animation-duration: $dur-loop;
animation-iteration-count: infinite;
animation-timing-function: $easing;
animation-name: fw-ptr;
}
.fw.is-demo .fw__tab--1 {
animation-duration: $dur-loop;
animation-iteration-count: infinite;
animation-timing-function: steps(1, end);
animation-name: fw-tab1;
}
.fw.is-demo .fw__tab--2 {
animation-duration: $dur-loop;
animation-iteration-count: infinite;
animation-timing-function: steps(1, end);
animation-name: fw-tab2;
}
.fw.is-demo .fw__panel--1 {
animation-duration: $dur-loop;
animation-iteration-count: infinite;
animation-timing-function: steps(1, end);
animation-name: fw-panel1;
}
.fw.is-demo .fw__panel--2 {
animation-duration: $dur-loop;
animation-iteration-count: infinite;
animation-timing-function: steps(1, end);
animation-name: fw-panel2;
}
.fw.is-demo .fw__filenameDemo--1 {
animation-duration: $dur-loop;
animation-iteration-count: infinite;
animation-timing-function: steps(1, end);
animation-name: fw-filename1;
}
.fw.is-demo .fw__filenameDemo--2 {
animation-duration: $dur-loop;
animation-iteration-count: infinite;
animation-timing-function: steps(1, end);
animation-name: fw-filename2;
}
.fw.is-demo .fw__dot--1 {
animation-duration: $dur-instant;
animation-iteration-count: 1;
animation-timing-function: $ease-pop;
animation-fill-mode: backwards;
animation-delay: 360ms;
animation-name: fw-dot-pop;
}
.fw.is-demo .fw__dot--2 {
animation-duration: $dur-instant;
animation-iteration-count: 1;
animation-timing-function: $ease-pop;
animation-fill-mode: backwards;
animation-delay: 430ms;
animation-name: fw-dot-pop;
}
.fw.is-demo .fw__dot--3 {
animation-duration: $dur-instant;
animation-iteration-count: 1;
animation-timing-function: $ease-pop;
animation-fill-mode: backwards;
animation-delay: 500ms;
animation-name: fw-dot-pop;
}
// 끝 좌표만 수정: 132px(88px 이동) 지점이 켜진 탭 라벨과 겹쳐 글자를 가렸다 →
// 탭 오른쪽 빈 자리(214px)로 옮긴다. 경로(퍼센트·이징)는 그대로.
@keyframes fw-ptr {
0% { transform: translate(0, 0) scale(1); }
16% { transform: translate(170px, 0) scale(1); animation-timing-function: $ease-pop; }
18% { transform: translate(170px, 0) scale(.86); animation-timing-function: $ease-pop; }
22% { transform: translate(170px, 0) scale(1); }
34% { transform: translate(170px, 0) scale(1); animation-timing-function: $easing; }
66% { transform: translate(0, 0) scale(1); }
100% { transform: translate(0, 0) scale(1); }
}
@keyframes fw-tab1 {
0% { background: $tab-on-bg; color: $tab-on-fg; transform: translateY(-2px); }
18% { background: $tab-off-bg; color: $tab-off-fg; transform: translateY(0); }
84% { background: $tab-on-bg; color: $tab-on-fg; transform: translateY(-2px); }
100% { background: $tab-on-bg; color: $tab-on-fg; transform: translateY(-2px); }
}
@keyframes fw-tab2 {
0% { background: $tab-off-bg; color: $tab-off-fg; transform: translateY(0); }
18% { background: $tab-on-bg; color: $tab-on-fg; transform: translateY(-2px); }
84% { background: $tab-off-bg; color: $tab-off-fg; transform: translateY(0); }
100% { background: $tab-off-bg; color: $tab-off-fg; transform: translateY(0); }
}
@keyframes fw-panel1 {
0% { opacity: 1; visibility: visible; }
18% { opacity: 0; visibility: hidden; }
84% { opacity: 1; visibility: visible; }
100% { opacity: 1; visibility: visible; }
}
@keyframes fw-panel2 {
0% { opacity: 0; visibility: hidden; }
18% { opacity: 1; visibility: visible; }
84% { opacity: 0; visibility: hidden; }
100% { opacity: 0; visibility: hidden; }
}
@keyframes fw-filename1 {
0% { opacity: 1; }
18% { opacity: 0; }
84% { opacity: 1; }
100% { opacity: 1; }
}
@keyframes fw-filename2 {
0% { opacity: 0; }
18% { opacity: 1; }
84% { opacity: 0; }
100% { opacity: 0; }
}
@keyframes fw-dot-pop {
0% { transform: scale(1); }
50% { transform: scale(1.18); }
100% { transform: scale(1); }
}
@media (max-width: 340px) {
// 폭은 이미 max-width: 100% 라 289px 언저리를 넘지 않는다(320px 실폭 292px 예산 보호) — 몸통 높이만 줄인다
.fw__body { height: 96px; }
.fw__panel { font-size: 10px; line-height: 16px; padding: $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
.fw.is-demo .fw__ptr,
.fw.is-demo .fw__tab--1,
.fw.is-demo .fw__tab--2,
.fw.is-demo .fw__panel--1,
.fw.is-demo .fw__panel--2,
.fw.is-demo .fw__filenameDemo--1,
.fw.is-demo .fw__filenameDemo--2,
.fw.is-demo .fw__dot--1,
.fw.is-demo .fw__dot--2,
.fw.is-demo .fw__dot--3 { animation: none !important; }
.fw.is-demo .fw__ptr { opacity: 0; }
.fw.is-demo .fw__tab--1 { background: $tab-off-bg; color: $tab-off-fg; transform: translateY(0); }
.fw.is-demo .fw__tab--2 { background: $tab-on-bg; color: $tab-on-fg; transform: translateY(-2px); }
.fw.is-demo .fw__panel--1 { opacity: 0; visibility: hidden; }
.fw.is-demo .fw__panel--2 { opacity: 1; visibility: visible; }
.fw.is-demo .fw__filenameDemo--1 { opacity: 0; }
.fw.is-demo .fw__filenameDemo--2 { opacity: 1; }
}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);
}
.fw {
box-sizing: border-box;
position: relative;
width: 288px;
max-width: 100%;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.fw__head {
display: flex;
align-items: center;
gap: 8px;
height: 28px;
padding: 0 12px;
}
.fw__dots {
display: inline-flex;
align-items: center;
gap: 4px;
flex: 0 0 auto;
}
.fw__dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.fw__dot--1 {
background: #c2361a;
}
.fw__dot--2 {
background: #ffd23f;
}
.fw__dot--3 {
background: #4cd4a6;
}
.fw__title {
display: inline-flex;
align-items: center;
gap: 4px;
min-width: 0;
color: rgba(255, 247, 230, 0.7);
}
.fw__icon {
display: inline-flex;
flex: 0 0 auto;
}
.fw__icon svg {
display: block;
}
.fw__filenameSlot {
position: relative;
display: inline-block;
min-width: 0;
}
.fw__filename,
.fw__filenameDemo {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
font-size: 11px;
font-weight: 700;
line-height: 1;
}
.fw__filenameDemo {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
}
.fw.is-demo .fw__filename {
opacity: 0;
}
.fw__tabs {
position: relative;
display: flex;
align-items: flex-end;
gap: 4px;
padding: 0 12px;
background: rgba(0, 0, 0, 0.22);
}
.fw__ptr {
position: absolute;
left: 44px;
top: 4px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #17141a;
border: 2px solid #fff7e6;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
pointer-events: none;
opacity: 0;
transform: translate(0, 0) scale(1);
}
.fw__tab {
position: relative;
display: inline-flex;
align-items: center;
height: 24px;
padding: 0 12px;
border: 0;
border-radius: 4px;
background: rgba(255, 247, 230, 0);
color: rgba(255, 247, 230, 0.5);
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
font-size: 11px;
font-weight: 700;
line-height: 1;
cursor: pointer;
transform: translateY(0);
}
.fw__tab:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: -2px;
}
.fw__tab[aria-selected=true] {
background: rgba(255, 247, 230, 0.14);
color: #fff7e6;
transform: translateY(-2px);
}
.fw__body {
position: relative;
height: 114px;
overflow: hidden;
}
.fw__panel {
position: absolute;
inset: 0;
box-sizing: border-box;
margin: 0;
padding: 12px;
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
font-size: 11px;
line-height: 18px;
color: #fff7e6;
white-space: pre;
opacity: 0;
visibility: hidden;
}
.fw__panel.is-active {
opacity: 1;
visibility: visible;
}
.fw__panel .k {
color: #ff4d1f;
}
.fw__panel .s {
color: #ffd23f;
}
.fw__panel .n {
color: #6ec8ff;
}
.fw__panel .c {
color: rgba(255, 247, 230, 0.72);
}
.fw.is-demo .fw__ptr {
opacity: 1;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-name: fw-ptr;
}
.fw.is-demo .fw__tab--1 {
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: steps(1, end);
animation-name: fw-tab1;
}
.fw.is-demo .fw__tab--2 {
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: steps(1, end);
animation-name: fw-tab2;
}
.fw.is-demo .fw__panel--1 {
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: steps(1, end);
animation-name: fw-panel1;
}
.fw.is-demo .fw__panel--2 {
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: steps(1, end);
animation-name: fw-panel2;
}
.fw.is-demo .fw__filenameDemo--1 {
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: steps(1, end);
animation-name: fw-filename1;
}
.fw.is-demo .fw__filenameDemo--2 {
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: steps(1, end);
animation-name: fw-filename2;
}
.fw.is-demo .fw__dot--1 {
animation-duration: 140ms;
animation-iteration-count: 1;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-fill-mode: backwards;
animation-delay: 360ms;
animation-name: fw-dot-pop;
}
.fw.is-demo .fw__dot--2 {
animation-duration: 140ms;
animation-iteration-count: 1;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-fill-mode: backwards;
animation-delay: 430ms;
animation-name: fw-dot-pop;
}
.fw.is-demo .fw__dot--3 {
animation-duration: 140ms;
animation-iteration-count: 1;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-fill-mode: backwards;
animation-delay: 500ms;
animation-name: fw-dot-pop;
}
@keyframes fw-ptr {
0% {
transform: translate(0, 0) scale(1);
}
16% {
transform: translate(170px, 0) scale(1);
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
}
18% {
transform: translate(170px, 0) scale(0.86);
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
}
22% {
transform: translate(170px, 0) scale(1);
}
34% {
transform: translate(170px, 0) scale(1);
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
}
66% {
transform: translate(0, 0) scale(1);
}
100% {
transform: translate(0, 0) scale(1);
}
}
@keyframes fw-tab1 {
0% {
background: rgba(255, 247, 230, 0.14);
color: #fff7e6;
transform: translateY(-2px);
}
18% {
background: rgba(255, 247, 230, 0);
color: rgba(255, 247, 230, 0.5);
transform: translateY(0);
}
84% {
background: rgba(255, 247, 230, 0.14);
color: #fff7e6;
transform: translateY(-2px);
}
100% {
background: rgba(255, 247, 230, 0.14);
color: #fff7e6;
transform: translateY(-2px);
}
}
@keyframes fw-tab2 {
0% {
background: rgba(255, 247, 230, 0);
color: rgba(255, 247, 230, 0.5);
transform: translateY(0);
}
18% {
background: rgba(255, 247, 230, 0.14);
color: #fff7e6;
transform: translateY(-2px);
}
84% {
background: rgba(255, 247, 230, 0);
color: rgba(255, 247, 230, 0.5);
transform: translateY(0);
}
100% {
background: rgba(255, 247, 230, 0);
color: rgba(255, 247, 230, 0.5);
transform: translateY(0);
}
}
@keyframes fw-panel1 {
0% {
opacity: 1;
visibility: visible;
}
18% {
opacity: 0;
visibility: hidden;
}
84% {
opacity: 1;
visibility: visible;
}
100% {
opacity: 1;
visibility: visible;
}
}
@keyframes fw-panel2 {
0% {
opacity: 0;
visibility: hidden;
}
18% {
opacity: 1;
visibility: visible;
}
84% {
opacity: 0;
visibility: hidden;
}
100% {
opacity: 0;
visibility: hidden;
}
}
@keyframes fw-filename1 {
0% {
opacity: 1;
}
18% {
opacity: 0;
}
84% {
opacity: 1;
}
100% {
opacity: 1;
}
}
@keyframes fw-filename2 {
0% {
opacity: 0;
}
18% {
opacity: 1;
}
84% {
opacity: 0;
}
100% {
opacity: 0;
}
}
@keyframes fw-dot-pop {
0% {
transform: scale(1);
}
50% {
transform: scale(1.18);
}
100% {
transform: scale(1);
}
}
@media (max-width: 340px) {
.fw__body {
height: 96px;
}
.fw__panel {
font-size: 10px;
line-height: 16px;
padding: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.fw.is-demo .fw__ptr,
.fw.is-demo .fw__tab--1,
.fw.is-demo .fw__tab--2,
.fw.is-demo .fw__panel--1,
.fw.is-demo .fw__panel--2,
.fw.is-demo .fw__filenameDemo--1,
.fw.is-demo .fw__filenameDemo--2,
.fw.is-demo .fw__dot--1,
.fw.is-demo .fw__dot--2,
.fw.is-demo .fw__dot--3 {
animation: none !important;
}
.fw.is-demo .fw__ptr {
opacity: 0;
}
.fw.is-demo .fw__tab--1 {
background: rgba(255, 247, 230, 0);
color: rgba(255, 247, 230, 0.5);
transform: translateY(0);
}
.fw.is-demo .fw__tab--2 {
background: rgba(255, 247, 230, 0.14);
color: #fff7e6;
transform: translateY(-2px);
}
.fw.is-demo .fw__panel--1 {
opacity: 0;
visibility: hidden;
}
.fw.is-demo .fw__panel--2 {
opacity: 1;
visibility: visible;
}
.fw.is-demo .fw__filenameDemo--1 {
opacity: 0;
}
.fw.is-demo .fw__filenameDemo--2 {
opacity: 1;
}
}var root = document.getElementById('fw');
var tabs = [document.getElementById('fw-tab1'), document.getElementById('fw-tab2')];
var panels = [document.getElementById('fw-panel1'), document.getElementById('fw-panel2')];
var titleEl = document.getElementById('fw-title');
function fwSelect(i) {
root.classList.remove('is-demo');
// 머리의 파일 이름이 켜진 탭을 따라간다 — 거짓 헤더 방지
titleEl.textContent = tabs[i].textContent;
tabs.forEach(function (t, idx) {
var on = idx === i;
t.setAttribute('aria-selected', on ? 'true' : 'false');
t.tabIndex = on ? 0 : -1;
});
panels.forEach(function (p, idx) {
var on = idx === i;
p.classList.toggle('is-active', on);
p.setAttribute('aria-hidden', on ? 'false' : 'true');
});
}
tabs.forEach(function (t, i) {
t.addEventListener('click', function () {
fwSelect(i);
t.focus({ preventScroll: true });
});
t.addEventListener('keydown', function (e) {
if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;
e.preventDefault();
var next = (i + (e.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length;
fwSelect(next);
tabs[next].focus({ preventScroll: true });
});
});import { useState, useRef } from "react";
import type { CSSProperties } from "react";
import styles from "./FilenameWindowHeader.module.scss";
export type FilenameWindowHeaderProps = {
filenames?: [string, string];
tabsLabel?: string;
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type KeyEvt = { key: string; preventDefault(): void };
type Box<T> = { current: T | null };
const FileCodeIcon = () => (
<svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true">
<g fill="currentColor">
<path d="M208 88h-56V32Z" opacity=".2" />
<path d="M181.66 146.34a8 8 0 0 1 0 11.32l-24 24a8 8 0 0 1-11.32-11.32L164.69 152l-18.35-18.34a8 8 0 0 1 11.32-11.32Zm-72-24a8 8 0 0 0-11.32 0l-24 24a8 8 0 0 0 0 11.32l24 24a8 8 0 0 0 11.32-11.32L91.31 152l18.35-18.34a8 8 0 0 0 0-11.32M216 88v128a16 16 0 0 1-16 16H56a16 16 0 0 1-16-16V40a16 16 0 0 1 16-16h96a8 8 0 0 1 5.66 2.34l56 56A8 8 0 0 1 216 88m-56-8h28.69L160 51.31Zm40 136V96h-48a8 8 0 0 1-8-8V40H56v176z" />
</g>
</svg>
);
const HtmlCode = () => (
<>
<span className={styles.c}><</span><span className={styles.k}>section</span> class=<span className={styles.s}>"card"</span><span className={styles.c}>></span>
{"\n "}<span className={styles.c}><</span><span className={styles.k}>h2</span><span className={styles.c}>></span>Hello<span className={styles.c}></</span><span className={styles.k}>h2</span><span className={styles.c}>></span>
{"\n "}<span className={styles.c}><</span><span className={styles.k}>p</span><span className={styles.c}>></span>Ship it.<span className={styles.c}></</span><span className={styles.k}>p</span><span className={styles.c}>></span>
{"\n "}<span className={styles.c}><</span><span className={styles.k}>button</span><span className={styles.c}>></span>Go<span className={styles.c}></</span><span className={styles.k}>button</span><span className={styles.c}>></span>
{"\n"}<span className={styles.c}></</span><span className={styles.k}>section</span><span className={styles.c}>></span>
</>
);
const CssCode = () => (
<>
<span className={styles.k}>.card</span> <span className={styles.c}>{"{"}</span>
{"\n "}padding: <span className={styles.n}>16px</span><span className={styles.c}>;</span>
{"\n "}border-radius: <span className={styles.n}>12px</span><span className={styles.c}>;</span>
{"\n "}box-shadow: <span className={styles.n}>0 2px 8px</span><span className={styles.c}>;</span>
{"\n"}<span className={styles.c}>{"}"}</span>
</>
);
export default function FilenameWindowHeader({
filenames = ["index.html", "style.css"],
tabsLabel = "Files",
demo = true,
duration = 220,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: FilenameWindowHeaderProps) {
const [live, setLive] = useState(demo);
const [active, setActive] = useState(0);
const tab1Ref = useRef<HTMLButtonElement>(null) as Box<HTMLButtonElement>;
const tab2Ref = useRef<HTMLButtonElement>(null) as Box<HTMLButtonElement>;
const tabRefs: Box<HTMLButtonElement>[] = [tab1Ref, tab2Ref];
function select(i: number) {
setLive(false);
setActive(i);
}
function onTabKeyDown(e: KeyEvt, i: number) {
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
e.preventDefault();
const next = (i + (e.key === "ArrowRight" ? 1 : -1) + tabRefs.length) % tabRefs.length;
select(next);
const el = tabRefs[next].current;
if (el) el.focus({ preventScroll: true });
}
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const rootClass = [styles.root, live ? styles.isDemo : ""].join(" ");
return (
<div className={rootClass} style={vars}>
<div className={styles.head}>
<span className={styles.dots} aria-hidden="true">
<span className={[styles.dot, styles.dot1].join(" ")} />
<span className={[styles.dot, styles.dot2].join(" ")} />
<span className={[styles.dot, styles.dot3].join(" ")} />
</span>
<span className={styles.title}>
<span className={styles.icon} aria-hidden="true"><FileCodeIcon /></span>
<span className={styles.filenameSlot}>
<span className={styles.filename} id="fw-react-title">{filenames[active]}</span>
<span className={[styles.filenameDemo, styles.filenameDemo1].join(" ")} aria-hidden="true">{filenames[0]}</span>
<span className={[styles.filenameDemo, styles.filenameDemo2].join(" ")} aria-hidden="true">{filenames[1]}</span>
</span>
</span>
</div>
<div className={styles.tabs} role="tablist" aria-label={tabsLabel}>
<span className={styles.ptr} aria-hidden="true" />
{filenames.map((name, i) => (
<button
key={name}
ref={tabRefs[i]}
className={[styles.tab, i === 0 ? styles.tab1 : styles.tab2].join(" ")}
type="button"
role="tab"
aria-selected={live ? undefined : active === i}
aria-controls={`fw-react-panel${i}`}
tabIndex={live ? -1 : active === i ? 0 : -1}
onClick={() => select(i)}
onKeyDown={(e: KeyEvt) => onTabKeyDown(e, i)}
>
{name}
</button>
))}
</div>
<div className={styles.body}>
<pre
className={[styles.panel, styles.panel1, !live && active === 0 ? styles.isActive : ""].join(" ")}
id="fw-react-panel0"
role="tabpanel"
aria-hidden={live ? undefined : active !== 0}
>
<code><HtmlCode /></code>
</pre>
<pre
className={[styles.panel, styles.panel2, !live && active === 1 ? styles.isActive : ""].join(" ")}
id="fw-react-panel1"
role="tabpanel"
aria-hidden={live ? undefined : active !== 1}
>
<code><CssCode /></code>
</pre>
</div>
</div>
);
}06 Window header that carries the file name — Measured area 5.082% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · 9 Code Block UI Patterns — No Highlighter Library
The post this demo lives in
9 Code Block UI Patterns — No Highlighter LibraryA code block UI is the small frame built around a snippet — header, copy button, tabs — that tells a reader what they ar See all demos in this post


