01Install command copy box
Tap the npm, pnpm, or yarn tab and the install line rewrites itself in that tool's syntax. The copy button really writes to the clipboard through navigator.clipboard, and the confirmation is announced once via aria-live.
Best forThe getting-started section of open-source docs, install block on a library landing page
navigator.clipboardaria-livesteps(1, end)

<main class="stage">
<div class="ic is-demo" id="ic" data-mgr="npm">
<div class="ic__tabs" role="tablist" aria-label="Package manager">
<button class="ic__tab" type="button" role="tab" id="tabNpm" aria-selected="true" data-mgr="npm">npm</button>
<button class="ic__tab" type="button" role="tab" id="tabPnpm" aria-selected="false" data-mgr="pnpm">pnpm</button>
<button class="ic__tab" type="button" role="tab" id="tabYarn" aria-selected="false" data-mgr="yarn">yarn</button>
</div>
<div class="ic__row">
<span class="ic__sign" aria-hidden="true">$</span>
<span class="ic__slot">
<code class="ic__cmd" data-mgr="npm">npm i <b>motionkit</b></code>
<code class="ic__cmd" data-mgr="pnpm">pnpm add <b>motionkit</b></code>
<code class="ic__cmd" data-mgr="yarn">yarn add <b>motionkit</b></code>
</span>
<button class="ic__copy" type="button" id="copyBtn">
<span class="ic__ico" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M216 40v128h-48V88H88V40Z" opacity=".2"/><path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8m-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z"/></g></svg></span>
<span class="ic__word">Copy command</span>
</button>
</div>
<p class="ic__note" id="note" aria-live="polite">Copied to your clipboard</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 설치 명령 복사 상자 — 같은 패키지를 npm·pnpm·yarn 문법으로 갈아 끼우고 그 줄을 클립보드에 넣는다.
// 명령 세 줄은 같은 격자 칸(grid-area: 1 / 1)에 겹쳐 두어 문법이 바뀌어도 줄 높이가 흔들리지 않는다.
// 격자 미리보기(.is-demo)는 npm → pnpm → yarn 을 steps(1, end) 로 컷 전환하고,
// 그 사이 복사 버튼이 한 번 채워졌다가 90% 안에 되돌아오는 2초 루프.
// 고정폭 서체는 font 단축이 아니라 font-family 낱개로 준다 (BD20).
$duration: $dur-quick;
$easing: $ease-spring;
$color: $chart-3;
$mono: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", monospace;
$line: rgba(255, 255, 255, .16);
$dim: rgba(255, 255, 255, .58);
@include stage($stage-ink);
.ic {
display: flex; flex-direction: column; gap: $sp-2;
box-sizing: border-box; width: min(100%, 400px);
padding: $sp-3; border: 1px solid $line; border-radius: $r-card;
background: rgba(255, 255, 255, .05);
font-family: $mono;
}
// ── 관리자 탭: 인디케이터를 밀지 않고 탭마다 자기 밑줄을 켠다 (BD19 회피)
.ic__tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: $sp-1; }
.ic__tab {
box-sizing: border-box; width: 100%; padding: $sp-1 0;
border: 0; border-bottom: 2px solid transparent; border-radius: 0;
background: none; color: $dim; cursor: pointer;
font-family: inherit; font-weight: 700; font-size: clamp(11px, 3vw, 12px); line-height: 1.4;
transition: color $duration $easing, border-color $duration $easing;
}
.ic__tab[aria-selected="true"] { color: $subject-cream; border-bottom-color: $color; }
.ic__tab:focus-visible { outline: 2px solid $subject-blue; outline-offset: 2px; }
// ── 명령 줄
.ic__row { display: flex; align-items: center; gap: $sp-2; min-width: 0; }
.ic__sign { flex: 0 0 auto; color: $color; font-weight: 700; font-size: clamp(12px, 3.2vw, 13px); line-height: 1.4; }
.ic__slot { display: grid; flex: 1 1 auto; min-width: 0; }
.ic__slot > * { grid-area: 1 / 1; }
.ic__cmd {
color: $subject-cream; font-family: inherit; font-weight: 500;
font-size: clamp(11px, 3vw, 13px); line-height: 1.4;
white-space: nowrap; overflow: hidden; visibility: hidden;
}
.ic__cmd b { color: $stage-yellow; font-weight: 700; }
.ic[data-mgr="npm"] .ic__cmd[data-mgr="npm"],
.ic[data-mgr="pnpm"] .ic__cmd[data-mgr="pnpm"],
.ic[data-mgr="yarn"] .ic__cmd[data-mgr="yarn"] { visibility: visible; }
// ── 복사 버튼: 채움은 배경 요소를 scaleX 로 늘려 왼쪽 변에서 자란다 (blog-demo 규칙 8 의 붙는 지점)
.ic__copy {
position: relative; overflow: hidden; flex: 0 0 auto;
display: inline-flex; align-items: center; gap: $sp-1;
box-sizing: border-box; min-height: 28px; padding: 0 $sp-2;
border: 1px solid $line; border-radius: $r-control;
background: rgba(255, 255, 255, .06); color: $subject-cream; cursor: pointer;
font-family: inherit; font-weight: 700; font-size: clamp(10px, 2.8vw, 11px); line-height: 1;
}
.ic__copy::before {
content: ""; position: absolute; inset: 0; background: $color;
transform: scaleX(0); transform-origin: left center;
transition: transform $duration $easing;
}
.ic.is-done .ic__copy::before { transform: scaleX(1); }
.ic.is-done .ic__copy { color: $stage-ink; }
.ic__copy:focus-visible { outline: 2px solid $subject-blue; outline-offset: 2px; }
.ic__ico, .ic__word { position: relative; }
.ic__ico { display: grid; place-items: center; font-size: 14px; }
// ── 상태 줄: 복사 뒤에만 보인다. 자리는 늘 차지해 줄 수가 흔들리지 않는다.
// aria-live="polite" 를 단 칸이라 글자가 다시 들어오는 순간 한 번 읽힌다
.ic__note {
margin: 0; min-height: 14px; color: $color; visibility: hidden;
font-family: inherit; font-weight: 500; font-size: clamp(10px, 2.6vw, 11px); line-height: 1.3;
transform: translateY(4px); transition: transform $duration $easing;
}
.ic.is-done .ic__note { visibility: visible; transform: translateY(0); }
// ── 격자 미리보기 루프 (2초)
// 0~33% npm · 34~66% pnpm · 67~100% yarn 을 컷으로 갈고, 8~30% 에 복사 채움이 지나간다.
// 되돌림은 0% 경계에서만 일어나 복귀 프레임이 poster 로 뽑히지 않는다 (blog-demo 「복귀 90%」)
.ic.is-demo .ic__cmd,
.ic.is-demo .ic__tab {
animation-duration: $dur-loop; animation-timing-function: steps(1, end);
animation-iteration-count: infinite; animation-fill-mode: backwards;
}
.ic.is-demo .ic__cmd[data-mgr="npm"] { animation-name: icShow1; }
.ic.is-demo .ic__cmd[data-mgr="pnpm"] { animation-name: icShow2; }
.ic.is-demo .ic__cmd[data-mgr="yarn"] { animation-name: icShow3; }
@keyframes icShow1 {
0%, 33% { visibility: visible; }
34%, 100% { visibility: hidden; }
}
@keyframes icShow2 {
0%, 33% { visibility: hidden; }
34%, 66% { visibility: visible; }
67%, 100% { visibility: hidden; }
}
@keyframes icShow3 {
0%, 66% { visibility: hidden; }
67%, 100% { visibility: visible; }
}
.ic.is-demo .ic__tab:nth-child(1) { animation-name: icTab1; }
.ic.is-demo .ic__tab:nth-child(2) { animation-name: icTab2; }
.ic.is-demo .ic__tab:nth-child(3) { animation-name: icTab3; }
@keyframes icTab1 {
0%, 33% { color: $subject-cream; border-bottom-color: $color; }
34%, 100% { color: $dim; border-bottom-color: transparent; }
}
@keyframes icTab2 {
0%, 33% { color: $dim; border-bottom-color: transparent; }
34%, 66% { color: $subject-cream; border-bottom-color: $color; }
67%, 100% { color: $dim; border-bottom-color: transparent; }
}
@keyframes icTab3 {
0%, 66% { color: $dim; border-bottom-color: transparent; }
67%, 100% { color: $subject-cream; border-bottom-color: $color; }
}
// 채움은 연속 보간이라 steps 와 섞지 않는다 (266-09 실측: steps 짝은 clip-path·transform 을 컷으로 죽인다)
.ic.is-demo .ic__copy::before {
animation-name: icFill; animation-duration: $dur-loop; animation-timing-function: $easing;
animation-iteration-count: infinite; animation-fill-mode: backwards;
}
@keyframes icFill {
0%, 8% { transform: scaleX(0); }
20%, 74% { transform: scaleX(1); }
86%, 100% { transform: scaleX(0); }
}
.ic.is-demo .ic__copy {
animation-name: icBtn; animation-duration: $dur-loop; animation-timing-function: steps(1, end);
animation-iteration-count: infinite; animation-fill-mode: backwards;
}
@keyframes icBtn {
0%, 19% { color: $subject-cream; }
20%, 79% { color: $stage-ink; }
80%, 100% { color: $subject-cream; }
}
.ic.is-demo .ic__note {
animation-name: icNote; animation-duration: $dur-loop; animation-timing-function: $easing;
animation-iteration-count: infinite; animation-fill-mode: backwards;
}
@keyframes icNote {
0%, 19% { visibility: hidden; transform: translateY(4px); }
20% { visibility: visible; transform: translateY(4px); }
30%, 79% { visibility: visible; transform: translateY(0); }
80%, 100% { visibility: hidden; transform: translateY(0); }
}
@media (max-width: 340px) {
.ic { padding: $sp-2; gap: $sp-1; }
.ic__copy { padding: 0 $sp-1; }
}
@media (prefers-reduced-motion: reduce) {
.ic.is-demo .ic__cmd,
.ic.is-demo .ic__tab,
.ic.is-demo .ic__copy,
.ic.is-demo .ic__copy::before,
.ic.is-demo .ic__note { animation: none !important; }
.ic.is-demo .ic__cmd[data-mgr="npm"] { visibility: visible !important; }
.ic.is-demo .ic__cmd[data-mgr="pnpm"],
.ic.is-demo .ic__cmd[data-mgr="yarn"] { visibility: hidden !important; }
.ic__tab, .ic__copy::before, .ic__note { transition: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ic {
display: flex;
flex-direction: column;
gap: 8px;
box-sizing: border-box;
width: min(100%, 400px);
padding: 12px;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 16px;
background: rgba(255, 255, 255, 0.05);
font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", monospace;
}
.ic__tabs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
}
.ic__tab {
box-sizing: border-box;
width: 100%;
padding: 4px 0;
border: 0;
border-bottom: 2px solid transparent;
border-radius: 0;
background: none;
color: rgba(255, 255, 255, 0.58);
cursor: pointer;
font-family: inherit;
font-weight: 700;
font-size: clamp(11px, 3vw, 12px);
line-height: 1.4;
transition: color 220ms cubic-bezier(0.2, 0.8, 0.2, 1), border-color 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ic__tab[aria-selected=true] {
color: #fff7e6;
border-bottom-color: #2b9d74;
}
.ic__tab:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.ic__row {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.ic__sign {
flex: 0 0 auto;
color: #2b9d74;
font-weight: 700;
font-size: clamp(12px, 3.2vw, 13px);
line-height: 1.4;
}
.ic__slot {
display: grid;
flex: 1 1 auto;
min-width: 0;
}
.ic__slot > * {
grid-area: 1/1;
}
.ic__cmd {
color: #fff7e6;
font-family: inherit;
font-weight: 500;
font-size: clamp(11px, 3vw, 13px);
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
visibility: hidden;
}
.ic__cmd b {
color: #ffd23f;
font-weight: 700;
}
.ic[data-mgr=npm] .ic__cmd[data-mgr=npm],
.ic[data-mgr=pnpm] .ic__cmd[data-mgr=pnpm],
.ic[data-mgr=yarn] .ic__cmd[data-mgr=yarn] {
visibility: visible;
}
.ic__copy {
position: relative;
overflow: hidden;
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 4px;
box-sizing: border-box;
min-height: 28px;
padding: 0 8px;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 12px;
background: rgba(255, 255, 255, 0.06);
color: #fff7e6;
cursor: pointer;
font-family: inherit;
font-weight: 700;
font-size: clamp(10px, 2.8vw, 11px);
line-height: 1;
}
.ic__copy::before {
content: "";
position: absolute;
inset: 0;
background: #2b9d74;
transform: scaleX(0);
transform-origin: left center;
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ic.is-done .ic__copy::before {
transform: scaleX(1);
}
.ic.is-done .ic__copy {
color: #17141a;
}
.ic__copy:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.ic__ico, .ic__word {
position: relative;
}
.ic__ico {
display: grid;
place-items: center;
font-size: 14px;
}
.ic__note {
margin: 0;
min-height: 14px;
color: #2b9d74;
visibility: hidden;
font-family: inherit;
font-weight: 500;
font-size: clamp(10px, 2.6vw, 11px);
line-height: 1.3;
transform: translateY(4px);
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ic.is-done .ic__note {
visibility: visible;
transform: translateY(0);
}
.ic.is-demo .ic__cmd,
.ic.is-demo .ic__tab {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.ic.is-demo .ic__cmd[data-mgr=npm] {
animation-name: icShow1;
}
.ic.is-demo .ic__cmd[data-mgr=pnpm] {
animation-name: icShow2;
}
.ic.is-demo .ic__cmd[data-mgr=yarn] {
animation-name: icShow3;
}
@keyframes icShow1 {
0%, 33% {
visibility: visible;
}
34%, 100% {
visibility: hidden;
}
}
@keyframes icShow2 {
0%, 33% {
visibility: hidden;
}
34%, 66% {
visibility: visible;
}
67%, 100% {
visibility: hidden;
}
}
@keyframes icShow3 {
0%, 66% {
visibility: hidden;
}
67%, 100% {
visibility: visible;
}
}
.ic.is-demo .ic__tab:nth-child(1) {
animation-name: icTab1;
}
.ic.is-demo .ic__tab:nth-child(2) {
animation-name: icTab2;
}
.ic.is-demo .ic__tab:nth-child(3) {
animation-name: icTab3;
}
@keyframes icTab1 {
0%, 33% {
color: #fff7e6;
border-bottom-color: #2b9d74;
}
34%, 100% {
color: rgba(255, 255, 255, 0.58);
border-bottom-color: transparent;
}
}
@keyframes icTab2 {
0%, 33% {
color: rgba(255, 255, 255, 0.58);
border-bottom-color: transparent;
}
34%, 66% {
color: #fff7e6;
border-bottom-color: #2b9d74;
}
67%, 100% {
color: rgba(255, 255, 255, 0.58);
border-bottom-color: transparent;
}
}
@keyframes icTab3 {
0%, 66% {
color: rgba(255, 255, 255, 0.58);
border-bottom-color: transparent;
}
67%, 100% {
color: #fff7e6;
border-bottom-color: #2b9d74;
}
}
.ic.is-demo .ic__copy::before {
animation-name: icFill;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
@keyframes icFill {
0%, 8% {
transform: scaleX(0);
}
20%, 74% {
transform: scaleX(1);
}
86%, 100% {
transform: scaleX(0);
}
}
.ic.is-demo .ic__copy {
animation-name: icBtn;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
@keyframes icBtn {
0%, 19% {
color: #fff7e6;
}
20%, 79% {
color: #17141a;
}
80%, 100% {
color: #fff7e6;
}
}
.ic.is-demo .ic__note {
animation-name: icNote;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
@keyframes icNote {
0%, 19% {
visibility: hidden;
transform: translateY(4px);
}
20% {
visibility: visible;
transform: translateY(4px);
}
30%, 79% {
visibility: visible;
transform: translateY(0);
}
80%, 100% {
visibility: hidden;
transform: translateY(0);
}
}
@media (max-width: 340px) {
.ic {
padding: 8px;
gap: 4px;
}
.ic__copy {
padding: 0 4px;
}
}
@media (prefers-reduced-motion: reduce) {
.ic.is-demo .ic__cmd,
.ic.is-demo .ic__tab,
.ic.is-demo .ic__copy,
.ic.is-demo .ic__copy::before,
.ic.is-demo .ic__note {
animation: none !important;
}
.ic.is-demo .ic__cmd[data-mgr=npm] {
visibility: visible !important;
}
.ic.is-demo .ic__cmd[data-mgr=pnpm],
.ic.is-demo .ic__cmd[data-mgr=yarn] {
visibility: hidden !important;
}
.ic__tab, .ic__copy::before, .ic__note {
transition: none !important;
}
}var ic = document.getElementById('ic');
var copyBtn = document.getElementById('copyBtn');
var word = copyBtn.querySelector('.ic__word');
var note = document.getElementById('note');
var tabs = Array.prototype.slice.call(ic.querySelectorAll('.ic__tab'));
var doneTimer = 0;
function pick(mgr) {
ic.classList.remove('is-demo');
ic.setAttribute('data-mgr', mgr);
tabs.forEach(function (t) { t.setAttribute('aria-selected', String(t.dataset.mgr === mgr)); });
}
function currentCommand() {
var mgr = ic.getAttribute('data-mgr') || 'npm';
var line = ic.querySelector('.ic__cmd[data-mgr="' + mgr + '"]');
return line ? line.textContent : '';
}
function showDone() {
ic.classList.add('is-done');
word.textContent = 'Copied';
clearTimeout(doneTimer);
// 알림을 지웠다가 다시 넣어야 같은 문장이라도 aria-live 가 한 번 더 읽는다
note.textContent = '';
doneTimer = setTimeout(function () {
note.textContent = 'Copied to your clipboard';
doneTimer = setTimeout(function () { ic.classList.remove('is-done'); word.textContent = 'Copy command'; }, 1400);
}, 60);
}
tabs.forEach(function (t) { t.addEventListener('click', function () { pick(t.dataset.mgr); }); });
copyBtn.addEventListener('click', function () {
ic.classList.remove('is-demo');
var text = currentCommand();
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(showDone, showDone);
} else {
showDone();
}
});// InstallCopyTabs — vanilla 판(run/294/demo/01-terminal-install-copy-tabs/index.html) 의 React 포트.
// 같은 패키지를 npm·pnpm·yarn 문법으로 갈아 끼우고 그 줄을 클립보드에 넣는다.
// 탭이나 복사 버튼을 한 번 누르면 격자 미리보기 루프(demo)가 꺼지고 사람이 고른 상태만 남는다.
import { useEffect, useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./InstallCopyTabs.module.scss";
export type InstallCopyTabsProps = {
/** 격자 미리보기용 2초 자동 루프. 탭·복사 버튼을 누르면 꺼진다 */
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
/** React 이벤트 제네릭이 ambient 에 없어 쓰는 필드만 지역 타입으로 선언한다 */
type Box<T> = { current: T };
const MANAGERS = ["npm", "pnpm", "yarn"];
/** 관리자마다 설치 명령의 앞부분만 다르다 — 뒤에 붙는 패키지 이름은 같다 */
const VERBS: { [k: string]: string } = { npm: "npm i", pnpm: "pnpm add", yarn: "yarn add" };
const PACKAGE_NAME = "motionkit";
const NOTE_TEXT = "클립보드에 복사했습니다";
export default function InstallCopyTabs({
demo = true,
duration = 220,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#2b9d74",
}: InstallCopyTabsProps) {
const [live, setLive] = useState(demo);
const [mgr, setMgr] = useState("npm");
const [done, setDone] = useState(false);
const [note, setNote] = useState(NOTE_TEXT);
const stepTimer = useRef<number>(0) as Box<number>;
const backTimer = useRef<number>(0) as Box<number>;
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
useEffect(() => {
return () => {
window.clearTimeout(stepTimer.current);
window.clearTimeout(backTimer.current);
};
}, []);
function pick(next: string) {
setLive(false);
setMgr(next);
}
function showDone() {
setDone(true);
window.clearTimeout(stepTimer.current);
window.clearTimeout(backTimer.current);
// 알림을 지웠다가 다시 넣어야 같은 문장이라도 aria-live 가 한 번 더 읽는다
setNote("");
stepTimer.current = window.setTimeout(() => {
setNote(NOTE_TEXT);
backTimer.current = window.setTimeout(() => { setDone(false); }, 1400);
}, 60);
}
function copy() {
setLive(false);
const text = `${VERBS[mgr]} ${PACKAGE_NAME}`;
const clip = typeof navigator !== "undefined" ? navigator.clipboard : undefined;
if (clip) { clip.writeText(text).then(showDone, showDone); }
else { showDone(); }
}
const cls = [styles.root, live ? styles.isDemo : "", done ? styles.isDone : ""].filter(Boolean).join(" ");
return (
<main className={styles.stage} style={vars}>
<div className={cls} data-mgr={mgr}>
<div className={styles.tabs} role="tablist" aria-label="패키지 관리자">
{MANAGERS.map((name) => (
<button
className={styles.tab}
type="button"
role="tab"
key={name}
data-mgr={name}
aria-selected={mgr === name}
onClick={() => pick(name)}
>
{name}
</button>
))}
</div>
<div className={styles.row}>
<span className={styles.sign} aria-hidden="true">$</span>
<span className={styles.slot}>
{MANAGERS.map((name) => (
<code className={styles.cmd} key={name} data-mgr={name}>
{VERBS[name]} <b>{PACKAGE_NAME}</b>
</code>
))}
</span>
<button className={styles.copy} type="button" onClick={copy}>
<span className={styles.ico} aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M216 40v128h-48V88H88V40Z" opacity=".2" /><path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8m-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z" /></g></svg>
</span>
<span className={styles.word}>{done ? "복사됨" : "명령 복사"}</span>
</button>
</div>
<p className={styles.note} role="status">{note}</p>
</div>
</main>
);
}01 Install command copy box — Measured area 3.313% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · Terminal UI Design: 9 Copy-Paste CLI Parts
The post this demo lives in
Terminal UI Design: 9 Copy-Paste CLI PartsTerminal UI design is the craft of putting a command-line window inside a web page. These nine copy-paste CLI parts go f See all demos in this post


