07바코드 스캔 쿠폰
매대에서 보여주는 바코드 쿠폰입니다. 바코드는 repeating-linear-gradient 한 줄로 그리고, 스캔을 누르면 빛줄이 훑고 지나가는 자리부터 숫자가 읽혀 결제가 끝납니다.
쓰임매장 결제 화면, 멤버십 바코드 카드
repeating-linear-gradienttranslateYaria-live

<main class="stage">
<div class="bc is-demo" id="bc">
<div class="bc__card" id="card">
<div class="bc__head">
<p class="bc__title"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class="bc__ico" aria-hidden="true"><path d="M224,48V208H32V48Z" opacity="0.2"/><path d="M232,48V88a8,8,0,0,1-16,0V56H184a8,8,0,0,1,0-16h40A8,8,0,0,1,232,48ZM72,200H40V168a8,8,0,0,0-16,0v40a8,8,0,0,0,8,8H72a8,8,0,0,0,0-16Zm152-40a8,8,0,0,0-8,8v32H184a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V168A8,8,0,0,0,224,160ZM32,96a8,8,0,0,0,8-8V56H72a8,8,0,0,0,0-16H32a8,8,0,0,0-8,8V88A8,8,0,0,0,32,96ZM80,80a8,8,0,0,0-8,8v80a8,8,0,0,0,16,0V88A8,8,0,0,0,80,80Zm104,88V88a8,8,0,0,0-16,0v80a8,8,0,0,0,16,0ZM144,80a8,8,0,0,0-8,8v80a8,8,0,0,0,16,0V88A8,8,0,0,0,144,80Zm-32,0a8,8,0,0,0-8,8v80a8,8,0,0,0,16,0V88A8,8,0,0,0,112,80Z"/></svg><span>멤버십 바코드</span></p>
<span class="bc__state" id="stateTxt">대기 중</span>
</div>
<div class="bc__bars" aria-hidden="true">
<span class="bc__beam"></span>
</div>
<p class="bc__digits">8842 0071 3390</p>
<div class="bc__foot">
<button type="button" class="bc__btn" id="scanBtn">스캔</button>
<p class="bc__done" id="done"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class="bc__tick" aria-hidden="true"><path d="M232,56V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56Z" opacity="0.2"/><path d="M205.66,85.66l-96,96a8,8,0,0,1-11.32,0l-40-40a8,8,0,0,1,11.32-11.32L104,164.69l90.34-90.35a8,8,0,0,1,11.32,11.32Z"/></svg><span>결제 완료</span></p>
</div>
</div>
<p class="bc__live" id="live" aria-live="polite"></p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 바코드 스캔 쿠폰 — 바코드는 repeating-linear-gradient 두 겹(주기 4px·9px)로 불규칙한 막대를 만들고,
// 스캔 빔이 translateY 로 훑고 지나간 뒤 결제 완료 상태로 컷 전환한다.
$duration: $dur-quick;
$easing: $ease-out;
$color: $subject-cream;
@include stage($stage-orange);
.bc { display: flex; flex-direction: column; align-items: center; gap: $sp-2; width: 100%; }
.bc__card {
width: min(100%, 264px); background: $subject-cream; border-radius: $r-card;
box-shadow: $shadow-raised; padding: $sp-3 $sp-4; border: 2px solid transparent;
}
.bc__head { display: flex; align-items: center; justify-content: space-between; gap: $sp-2; }
.bc__title { display: flex; align-items: center; gap: $sp-1; margin: 0; font-weight: 800; font-size: clamp(11px, 3.2vw, 13px); color: $subject-ink; }
.bc__ico { width: 16px; height: 16px; color: $subject-ink; }
.bc__state {
background: rgba(23,20,26,.08); color: $subject-ink; border-radius: $r-pill;
font-weight: 700; font-size: clamp(9px, 2.6vw, 10px); padding: 0 $sp-2; line-height: 1.6;
}
.bc__bars {
position: relative; margin-top: $sp-2; height: 64px; border-radius: $r-sm; overflow: hidden;
background:
repeating-linear-gradient(90deg, $subject-ink 0 2px, transparent 2px 4px),
repeating-linear-gradient(90deg, $subject-ink 0 3px, transparent 3px 9px);
background-position: 0 0, 3px 0;
}
.bc__beam {
position: absolute; left: 0; right: 0; top: 0; height: 4px; visibility: hidden;
background: linear-gradient(180deg, transparent, rgba(76,212,166,.95) 40%, rgba(23,20,26,.9) 55%, transparent);
box-shadow: 0 0 8px rgba(76,212,166,.8);
}
.bc.is-scanning .bc__beam { visibility: visible; animation: bcBeamOnce 800ms ease-in-out; }
@keyframes bcBeamOnce { 0% { transform: translateY(0); } 100% { transform: translateY(60px); } }
.bc__digits { margin: $sp-2 0 0; font-weight: 800; font-size: clamp(14px, 4.2vw, 18px); letter-spacing: .3em; color: $subject-ink; text-align: center; }
.bc__foot { position: relative; display: flex; justify-content: center; margin-top: $sp-2; min-height: 30px; }
.bc__btn {
border: 0; cursor: pointer; background: $subject-ink; color: $subject-cream;
border-radius: $r-pill; font-family: inherit; font-weight: 800;
font-size: clamp(11px, 3vw, 12px); padding: $sp-1 $sp-5; line-height: 1.4;
transition: transform 140ms cubic-bezier(.2,.8,.2,1);
}
.bc__btn:active { transform: scale(.94); }
.bc__btn:focus-visible { outline: 3px solid $subject-blue; outline-offset: 2px; }
.bc__done { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: $sp-1; margin: 0; visibility: hidden; }
.bc.is-done .bc__done { visibility: visible; }
.bc.is-done .bc__btn { visibility: hidden; }
.bc.is-done .bc__bars { opacity: .3; }
.bc.is-done .bc__digits { text-decoration: line-through; color: rgba(23,20,26,.4); }
.bc__tick { width: 14px; height: 14px; color: #479074; }
.bc__done span { font-weight: 800; font-size: clamp(11px, 3vw, 12px); color: #479074; }
.bc__live { margin: 0; min-height: 1.2em; font-weight: 700; font-size: clamp(10px, 2.8vw, 11px); color: $subject-cream; }
// ── 격자 미리보기 루프: 빔이 위→아래로 훑고(10~68%) 74% 결제 완료 컷 → 94% 복귀
.bc.is-demo .bc__beam { visibility: visible; animation: bcBeamLoop 2s ease-in-out infinite; }
@keyframes bcBeamLoop {
0%, 10% { transform: translateY(0); }
68% { transform: translateY(60px); }
68.5%, 93% { transform: translateY(60px); opacity: 0; }
93.5%, 100% { transform: translateY(0); opacity: 1; }
}
.bc.is-demo .bc__btn { animation: bcBtnCut 2s steps(1, end) infinite; }
@keyframes bcBtnCut { 0%, 73% { visibility: visible; } 73.5%, 93% { visibility: hidden; } 93.5%, 100% { visibility: visible; } }
.bc.is-demo .bc__done { visibility: visible; }
.bc.is-demo .bc__done { animation: bcDonePop 2s ease-out infinite; }
@keyframes bcDonePop { 0%, 73% { transform: scale(0); } 77%, 92% { transform: scale(1); } 94%, 100% { transform: scale(0); } }
.bc.is-demo .bc__bars { animation: bcBarsCut 2s steps(1, end) infinite; }
@keyframes bcBarsCut { 0%, 73% { opacity: 1; } 73.5%, 93% { opacity: .3; } 93.5%, 100% { opacity: 1; } }
.bc.is-demo .bc__digits { animation: bcDigitsCut 2s steps(1, end) infinite; }
@keyframes bcDigitsCut { 0%, 73% { opacity: 1; } 73.5%, 93% { opacity: .4; } 93.5%, 100% { opacity: 1; } }
@media (max-width: 340px) {
.bc__card { padding: $sp-2 $sp-3; }
.bc__bars { height: 40px; margin-top: $sp-1; }
.bc__digits { margin-top: $sp-1; }
.bc__foot { margin-top: $sp-1; min-height: 26px; }
.bc { gap: $sp-1; }
}
@media (prefers-reduced-motion: reduce) {
.bc.is-demo .bc__beam,
.bc.is-demo .bc__btn,
.bc.is-demo .bc__done,
.bc.is-demo .bc__bars,
.bc.is-demo .bc__digits { animation: none !important; }
.bc__btn { transition: none !important; }
.bc.is-demo .bc__beam { transform: translateY(28px); }
.bc.is-demo .bc__done { transform: scale(1); }
.bc.is-demo .bc__btn { visibility: hidden; }
.bc.is-demo .bc__bars { opacity: .3; }
.bc.is-demo .bc__digits { opacity: .4; }
}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);
}
.bc {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: 100%;
}
.bc__card {
width: min(100%, 264px);
background: #fff7e6;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
padding: 12px 16px;
border: 2px solid transparent;
}
.bc__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.bc__title {
display: flex;
align-items: center;
gap: 4px;
margin: 0;
font-weight: 800;
font-size: clamp(11px, 3.2vw, 13px);
color: #17141a;
}
.bc__ico {
width: 16px;
height: 16px;
color: #17141a;
}
.bc__state {
background: rgba(23, 20, 26, 0.08);
color: #17141a;
border-radius: 999px;
font-weight: 700;
font-size: clamp(9px, 2.6vw, 10px);
padding: 0 8px;
line-height: 1.6;
}
.bc__bars {
position: relative;
margin-top: 8px;
height: 64px;
border-radius: 8px;
overflow: hidden;
background: repeating-linear-gradient(90deg, #17141a 0 2px, transparent 2px 4px), repeating-linear-gradient(90deg, #17141a 0 3px, transparent 3px 9px);
background-position: 0 0, 3px 0;
}
.bc__beam {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 4px;
visibility: hidden;
background: linear-gradient(180deg, transparent, rgba(76, 212, 166, 0.95) 40%, rgba(23, 20, 26, 0.9) 55%, transparent);
box-shadow: 0 0 8px rgba(76, 212, 166, 0.8);
}
.bc.is-scanning .bc__beam {
visibility: visible;
animation: bcBeamOnce 800ms ease-in-out;
}
@keyframes bcBeamOnce {
0% {
transform: translateY(0);
}
100% {
transform: translateY(60px);
}
}
.bc__digits {
margin: 8px 0 0;
font-weight: 800;
font-size: clamp(14px, 4.2vw, 18px);
letter-spacing: 0.3em;
color: #17141a;
text-align: center;
}
.bc__foot {
position: relative;
display: flex;
justify-content: center;
margin-top: 8px;
min-height: 30px;
}
.bc__btn {
border: 0;
cursor: pointer;
background: #17141a;
color: #fff7e6;
border-radius: 999px;
font-family: inherit;
font-weight: 800;
font-size: clamp(11px, 3vw, 12px);
padding: 4px 20px;
line-height: 1.4;
transition: transform 140ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.bc__btn:active {
transform: scale(0.94);
}
.bc__btn:focus-visible {
outline: 3px solid #2f6df6;
outline-offset: 2px;
}
.bc__done {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
margin: 0;
visibility: hidden;
}
.bc.is-done .bc__done {
visibility: visible;
}
.bc.is-done .bc__btn {
visibility: hidden;
}
.bc.is-done .bc__bars {
opacity: 0.3;
}
.bc.is-done .bc__digits {
text-decoration: line-through;
color: rgba(23, 20, 26, 0.4);
}
.bc__tick {
width: 14px;
height: 14px;
color: #479074;
}
.bc__done span {
font-weight: 800;
font-size: clamp(11px, 3vw, 12px);
color: #479074;
}
.bc__live {
margin: 0;
min-height: 1.2em;
font-weight: 700;
font-size: clamp(10px, 2.8vw, 11px);
color: #fff7e6;
}
.bc.is-demo .bc__beam {
visibility: visible;
animation: bcBeamLoop 2s ease-in-out infinite;
}
@keyframes bcBeamLoop {
0%, 10% {
transform: translateY(0);
}
68% {
transform: translateY(60px);
}
68.5%, 93% {
transform: translateY(60px);
opacity: 0;
}
93.5%, 100% {
transform: translateY(0);
opacity: 1;
}
}
.bc.is-demo .bc__btn {
animation: bcBtnCut 2s steps(1, end) infinite;
}
@keyframes bcBtnCut {
0%, 73% {
visibility: visible;
}
73.5%, 93% {
visibility: hidden;
}
93.5%, 100% {
visibility: visible;
}
}
.bc.is-demo .bc__done {
visibility: visible;
}
.bc.is-demo .bc__done {
animation: bcDonePop 2s ease-out infinite;
}
@keyframes bcDonePop {
0%, 73% {
transform: scale(0);
}
77%, 92% {
transform: scale(1);
}
94%, 100% {
transform: scale(0);
}
}
.bc.is-demo .bc__bars {
animation: bcBarsCut 2s steps(1, end) infinite;
}
@keyframes bcBarsCut {
0%, 73% {
opacity: 1;
}
73.5%, 93% {
opacity: 0.3;
}
93.5%, 100% {
opacity: 1;
}
}
.bc.is-demo .bc__digits {
animation: bcDigitsCut 2s steps(1, end) infinite;
}
@keyframes bcDigitsCut {
0%, 73% {
opacity: 1;
}
73.5%, 93% {
opacity: 0.4;
}
93.5%, 100% {
opacity: 1;
}
}
@media (max-width: 340px) {
.bc__card {
padding: 8px 12px;
}
.bc__bars {
height: 40px;
margin-top: 4px;
}
.bc__digits {
margin-top: 4px;
}
.bc__foot {
margin-top: 4px;
min-height: 26px;
}
.bc {
gap: 4px;
}
}
@media (prefers-reduced-motion: reduce) {
.bc.is-demo .bc__beam,
.bc.is-demo .bc__btn,
.bc.is-demo .bc__done,
.bc.is-demo .bc__bars,
.bc.is-demo .bc__digits {
animation: none !important;
}
.bc__btn {
transition: none !important;
}
.bc.is-demo .bc__beam {
transform: translateY(28px);
}
.bc.is-demo .bc__done {
transform: scale(1);
}
.bc.is-demo .bc__btn {
visibility: hidden;
}
.bc.is-demo .bc__bars {
opacity: 0.3;
}
.bc.is-demo .bc__digits {
opacity: 0.4;
}
}var bc = document.getElementById('bc');
var scanBtn = document.getElementById('scanBtn');
var stateTxt = document.getElementById('stateTxt');
var live = document.getElementById('live');
var bars = document.querySelector('.bc__bars');
scanBtn.addEventListener('click', function () {
bc.classList.remove('is-demo');
if (bc.classList.contains('is-done')) { return; }
bc.classList.add('is-scanning');
stateTxt.textContent = '스캔 중';
window.setTimeout(function () {
bc.classList.remove('is-scanning');
bc.classList.add('is-done');
stateTxt.textContent = '결제 완료';
live.textContent = '바코드를 읽어 결제를 끝냈어요';
}, 800);
});// BarcodeScanCoupon — vanilla 판(run/283/demo/07-coupon-barcode-scan/index.html) 의 React 포트.
// 스캔 버튼을 누르면 미리보기 루프에서 빠져나와 빔이 한 번 훑고(800ms) 결제 완료로 컷 전환한다.
import { useEffect, useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./BarcodeScanCoupon.module.scss";
export type BarcodeScanCouponProps = {
/** 격자 미리보기 루프 한 바퀴 (vanilla 2s) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
/** 빔이 바코드를 훑는 시간 — scss 의 bcBeamOnce 800ms 와 같은 값 */
const SCAN_MS = 800;
export default function BarcodeScanCoupon({
duration = 2000,
easing = "ease-out",
color = "#fff7e6",
}: BarcodeScanCouponProps) {
const [demo, setDemo] = useState(true);
const [scanning, setScanning] = useState(false);
const [done, setDone] = useState(false);
const [stateText, setStateText] = useState("대기 중");
const [liveText, setLiveText] = useState("");
const timerRef = useRef<number>(null);
useEffect(() => {
return () => {
if (timerRef.current !== null) { window.clearTimeout(timerRef.current); }
};
}, []);
function applyScan(): void {
setDemo(false);
if (done) { return; }
setScanning(true);
setStateText("스캔 중");
timerRef.current = window.setTimeout(() => {
setScanning(false);
setDone(true);
setStateText("결제 완료");
setLiveText("바코드를 읽어 결제를 끝냈어요");
}, SCAN_MS);
}
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const cls: string = [
styles.root,
demo ? styles.isDemo : "",
scanning ? styles.isScanning : "",
done ? styles.isDone : "",
].filter(Boolean).join(" ");
return (
<main className={styles.stage}>
<div className={cls} style={vars}>
<div className={styles.card}>
<div className={styles.head}>
<p className={styles.title}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" className={styles.ico} aria-hidden="true"><path d="M224,48V208H32V48Z" opacity="0.2" /><path d="M232,48V88a8,8,0,0,1-16,0V56H184a8,8,0,0,1,0-16h40A8,8,0,0,1,232,48ZM72,200H40V168a8,8,0,0,0-16,0v40a8,8,0,0,0,8,8H72a8,8,0,0,0,0-16Zm152-40a8,8,0,0,0-8,8v32H184a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V168A8,8,0,0,0,224,160ZM32,96a8,8,0,0,0,8-8V56H72a8,8,0,0,0,0-16H32a8,8,0,0,0-8,8V88A8,8,0,0,0,32,96ZM80,80a8,8,0,0,0-8,8v80a8,8,0,0,0,16,0V88A8,8,0,0,0,80,80Zm104,88V88a8,8,0,0,0-16,0v80a8,8,0,0,0,16,0ZM144,80a8,8,0,0,0-8,8v80a8,8,0,0,0,16,0V88A8,8,0,0,0,144,80Zm-32,0a8,8,0,0,0-8,8v80a8,8,0,0,0,16,0V88A8,8,0,0,0,112,80Z" /></svg>
<span>멤버십 바코드</span>
</p>
<span className={styles.state}>{stateText}</span>
</div>
<div className={styles.bars} aria-hidden="true">
<span className={styles.beam} />
</div>
<p className={styles.digits}>8842 0071 3390</p>
<div className={styles.foot}>
<button type="button" className={styles.btn} onClick={applyScan}>스캔</button>
<p className={styles.done}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" className={styles.tick} aria-hidden="true"><path d="M232,56V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56Z" opacity="0.2" /><path d="M205.66,85.66l-96,96a8,8,0,0,1-11.32,0l-40-40a8,8,0,0,1,11.32-11.32L104,164.69l90.34-90.35a8,8,0,0,1,11.32,11.32Z" /></svg>
<span>결제 완료</span>
</p>
</div>
</div>
<p className={styles.live} aria-live="polite">{liveText}</p>
</div>
</main>
);
}07 바코드 스캔 쿠폰 — 측정 누적면적 12.737% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 쿠폰 디자인 9종 — 도장부터 돌림판까지 복붙 코드
이 데모가 속한 글
쿠폰 디자인 9종 — 도장부터 돌림판까지 복붙 코드쿠폰 디자인은 할인을 받고 싶어지게 쿠폰 카드의 생김새와 움직임을 짜는 일입니다. 여기 아홉 가지는 노치 티켓·도장 적립판·돌림판·스크래치·바코드처럼 가게에서 실제로 쓰는 쿠폰 부품을 CSS 와 약간의 JS 로 만들 이 글의 데모 전부 보기


