01Notched ticket coupon
A ticket coupon with half-circle notches bitten out above and below the tear line. The notches are carved with mask and radial-gradient; pressing Save dips the button for a beat, then the wallet count ticks up.
Best forFirst-purchase coupon download card, promo landing coupon card
maskradial-gradientcubic-bezier

<main class="stage">
<div class="tk is-demo" id="tk">
<div class="tk__card" aria-label="First-purchase 30% ticket coupon">
<div class="tk__main">
<p class="tk__brand"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class="tk__ico" aria-hidden="true"><path d="M200,128a32,32,0,0,0,32,32v32a8,8,0,0,1-8,8H96V56H224a8,8,0,0,1,8,8V96A32,32,0,0,0,200,128Z" opacity="0.2"/><path d="M232,104a8,8,0,0,0,8-8V64a16,16,0,0,0-16-16H32A16,16,0,0,0,16,64V96a8,8,0,0,0,8,8,24,24,0,0,1,0,48,8,8,0,0,0-8,8v32a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V160a8,8,0,0,0-8-8,24,24,0,0,1,0-48ZM32,167.2a40,40,0,0,0,0-78.4V64H88V192H32Zm192,0V192H104V64H224V88.8a40,40,0,0,0,0,78.4Z"/></svg><span>WELCOME</span></p>
<p class="tk__pct">30<i>%</i></p>
<p class="tk__label">First-purchase discount</p>
<p class="tk__meta">Until midnight today · code WELCOME30</p>
</div>
<div class="tk__stub">
<button type="button" class="tk__btn" id="saveBtn" aria-pressed="false" aria-label="Save coupon to wallet">
<span class="tk__btnLabel tk__btnLabel--save">Save</span>
<span class="tk__btnLabel tk__btnLabel--done" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class="tk__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>Saved</span>
</button>
</div>
</div>
<p class="tk__wallet" id="wallet" aria-live="polite">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class="tk__ico" aria-hidden="true"><path d="M224,80V192a8,8,0,0,1-8,8H56a16,16,0,0,1-16-16V56A16,16,0,0,0,56,72H216A8,8,0,0,1,224,80Z" opacity="0.2"/><path d="M216,64H56a8,8,0,0,1,0-16H192a8,8,0,0,0,0-16H56A24,24,0,0,0,32,56V184a24,24,0,0,0,24,24H216a16,16,0,0,0,16-16V80A16,16,0,0,0,216,64Zm0,128H56a8,8,0,0,1-8-8V78.63A23.84,23.84,0,0,0,56,80H216Zm-48-60a12,12,0,1,1,12,12A12,12,0,0,1,168,132Z"/></svg>
<span>Wallet</span>
<span class="tk__count"><b class="tk__num tk__num--zero" aria-hidden="true">0</b><b class="tk__num tk__num--one" aria-hidden="true">1</b><span class="tk__srNum" id="srNum">0 coupons</span></span>
</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 노치 티켓 쿠폰 — 반원 노치는 mask + radial-gradient 로 진짜로 파고,
// 재단선은 점선 border. 격자 미리보기(.is-demo)는 2초 루프로 담기 동작을 보여준다.
$duration: $dur-loop;
$easing: $ease-pop;
$color: $subject-blue;
@include stage($stage-ink);
.tk { display: flex; flex-direction: column; align-items: center; gap: $sp-2; width: 100%; }
.tk__card {
display: flex; width: min(100%, 380px); background: $subject-cream; border-radius: $r-card;
box-shadow: $shadow-raised; overflow: hidden;
// 반원 노치 두 개 — 오른쪽 경계 72% 지점 위·아래. mask-composite 로 두 구멍을 동시에.
-webkit-mask:
radial-gradient(circle 10px at 72% 0, transparent 96%, #000 100%),
radial-gradient(circle 10px at 72% 100%, transparent 96%, #000 100%);
-webkit-mask-composite: destination-in;
mask:
radial-gradient(circle 10px at 72% 0, transparent 96%, #000 100%),
radial-gradient(circle 10px at 72% 100%, transparent 96%, #000 100%);
mask-composite: intersect;
}
.tk.is-demo .tk__card { animation: tkFloat 2s ease-in-out infinite; }
.tk__main { flex: 1 1 72%; padding: $sp-3 $sp-4; min-width: 0; }
.tk__brand { display: flex; align-items: center; gap: $sp-1; margin: 0; font-weight: 800; font-size: clamp(9px, 2.6vw, 11px); letter-spacing: .14em; color: rgba(23,20,26,.72); }
.tk__ico { width: 14px; height: 14px; flex: 0 0 auto; color: $subject-blue; }
.tk__pct { margin: $sp-1 0 0; font-weight: 800; font-size: clamp(34px, 10vw, 46px); line-height: 1; color: $subject-blue; }
.tk__pct i { font-style: normal; font-size: .55em; }
.tk__label { margin: $sp-1 0 0; font-weight: 700; font-size: clamp(11px, 3.2vw, 13px); color: $subject-ink; }
.tk__meta { margin: $sp-1 0 0; font-weight: 500; font-size: clamp(9px, 2.6vw, 10px); color: rgba(23,20,26,.68); }
.tk__stub {
flex: 0 0 28%; border-left: 2px dashed rgba(23,20,26,.22);
display: flex; align-items: center; justify-content: center; padding: $sp-3 $sp-2;
}
.tk__btn {
position: relative; border: 0; cursor: pointer; background: $subject-blue; color: $stage-paper;
border-radius: $r-pill; font-weight: 800; font-size: clamp(11px, 3vw, 12px);
padding: $sp-2 $sp-4; font-family: inherit; line-height: 1.2;
transition: transform 140ms cubic-bezier(.2,.8,.2,1);
}
.tk__btn:focus-visible { outline: 3px solid $stage-yellow; outline-offset: 2px; }
.tk__btnLabel { display: inline-flex; align-items: center; gap: 4px; }
.tk__btnLabel--done { position: absolute; inset: 0; justify-content: center; visibility: hidden; }
.tk.is-saved .tk__btnLabel--save { visibility: hidden; }
.tk.is-saved .tk__btnLabel--done { visibility: visible; }
.tk__tick { width: 12px; height: 12px; }
.tk__wallet {
display: inline-flex; align-items: center; gap: $sp-1; margin: 0;
background: rgba(255,247,230,.14); color: $subject-cream; border-radius: $r-pill;
padding: $sp-1 $sp-3; font-weight: 700; font-size: clamp(10px, 2.8vw, 12px);
}
.tk__wallet .tk__ico { color: $subject-cream; }
.tk__count { position: relative; display: inline-block; min-width: 2.2em; text-align: right; font-weight: 800; }
.tk__num { position: absolute; right: 0; top: 50%; transform: translateY(-50%); }
.tk__num--one { visibility: hidden; }
.tk.is-saved .tk__num--zero { visibility: hidden; }
.tk.is-saved .tk__num--one { visibility: visible; }
.tk__srNum { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); margin: 0; }
// ── 격자 미리보기 루프: 카드 둥둥(연속) + 버튼 눌림(42%) + 보관함 숫자 컷(55%) → 90% 복귀
@keyframes tkFloat { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
.tk.is-demo .tk__btn { animation: tkPress 2s $easing infinite; }
@keyframes tkPress {
0%, 40% { transform: scale(1); }
43% { transform: scale(.9); }
47%, 100% { transform: scale(1); }
}
.tk.is-demo .tk__btnLabel--save { animation: tkCutSave 2s steps(1, end) infinite; }
@keyframes tkCutSave { 0%, 45% { visibility: visible; } 45.5%, 89% { visibility: hidden; } 89.5%, 100% { visibility: visible; } }
.tk.is-demo .tk__btnLabel--done { animation: tkCutDone 2s steps(1, end) infinite; }
@keyframes tkCutDone { 0%, 45% { visibility: hidden; } 45.5%, 89% { visibility: visible; } 89.5%, 100% { visibility: hidden; } }
.tk.is-demo .tk__num--zero { animation: tkCutZero 2s steps(1, end) infinite; }
@keyframes tkCutZero { 0%, 54% { visibility: visible; } 54.5%, 89% { visibility: hidden; } 89.5%, 100% { visibility: visible; } }
.tk.is-demo .tk__num--one { animation: tkCutOne 2s steps(1, end) infinite; }
@keyframes tkCutOne { 0%, 54% { visibility: hidden; } 54.5%, 89% { visibility: visible; } 89.5%, 100% { visibility: hidden; } }
@media (max-width: 340px) {
.tk__main { padding: $sp-2 $sp-3; }
.tk__stub { padding: $sp-2 $sp-1; }
}
@media (prefers-reduced-motion: reduce) {
.tk.is-demo .tk__card,
.tk.is-demo .tk__btn,
.tk.is-demo .tk__btnLabel--save,
.tk.is-demo .tk__btnLabel--done,
.tk.is-demo .tk__num--zero,
.tk.is-demo .tk__num--one { animation: none !important; }
.tk__btn { 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);
}
.tk {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: 100%;
}
.tk__card {
display: flex;
width: min(100%, 380px);
background: #fff7e6;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
-webkit-mask: radial-gradient(circle 10px at 72% 0, transparent 96%, #000 100%), radial-gradient(circle 10px at 72% 100%, transparent 96%, #000 100%);
-webkit-mask-composite: destination-in;
mask: radial-gradient(circle 10px at 72% 0, transparent 96%, #000 100%), radial-gradient(circle 10px at 72% 100%, transparent 96%, #000 100%);
mask-composite: intersect;
}
.tk.is-demo .tk__card {
animation: tkFloat 2s ease-in-out infinite;
}
.tk__main {
flex: 1 1 72%;
padding: 12px 16px;
min-width: 0;
}
.tk__brand {
display: flex;
align-items: center;
gap: 4px;
margin: 0;
font-weight: 800;
font-size: clamp(9px, 2.6vw, 11px);
letter-spacing: 0.14em;
color: rgba(23, 20, 26, 0.72);
}
.tk__ico {
width: 14px;
height: 14px;
flex: 0 0 auto;
color: #2f6df6;
}
.tk__pct {
margin: 4px 0 0;
font-weight: 800;
font-size: clamp(34px, 10vw, 46px);
line-height: 1;
color: #2f6df6;
}
.tk__pct i {
font-style: normal;
font-size: 0.55em;
}
.tk__label {
margin: 4px 0 0;
font-weight: 700;
font-size: clamp(11px, 3.2vw, 13px);
color: #17141a;
}
.tk__meta {
margin: 4px 0 0;
font-weight: 500;
font-size: clamp(9px, 2.6vw, 10px);
color: rgba(23, 20, 26, 0.68);
}
.tk__stub {
flex: 0 0 28%;
border-left: 2px dashed rgba(23, 20, 26, 0.22);
display: flex;
align-items: center;
justify-content: center;
padding: 12px 8px;
}
.tk__btn {
position: relative;
border: 0;
cursor: pointer;
background: #2f6df6;
color: #ffffff;
border-radius: 999px;
font-weight: 800;
font-size: clamp(11px, 3vw, 12px);
padding: 8px 16px;
font-family: inherit;
line-height: 1.2;
transition: transform 140ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.tk__btn:focus-visible {
outline: 3px solid #ffd23f;
outline-offset: 2px;
}
.tk__btnLabel {
display: inline-flex;
align-items: center;
gap: 4px;
}
.tk__btnLabel--done {
position: absolute;
inset: 0;
justify-content: center;
visibility: hidden;
}
.tk.is-saved .tk__btnLabel--save {
visibility: hidden;
}
.tk.is-saved .tk__btnLabel--done {
visibility: visible;
}
.tk__tick {
width: 12px;
height: 12px;
}
.tk__wallet {
display: inline-flex;
align-items: center;
gap: 4px;
margin: 0;
background: rgba(255, 247, 230, 0.14);
color: #fff7e6;
border-radius: 999px;
padding: 4px 12px;
font-weight: 700;
font-size: clamp(10px, 2.8vw, 12px);
}
.tk__wallet .tk__ico {
color: #fff7e6;
}
.tk__count {
position: relative;
display: inline-block;
min-width: 2.2em;
text-align: right;
font-weight: 800;
}
.tk__num {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
.tk__num--one {
visibility: hidden;
}
.tk.is-saved .tk__num--zero {
visibility: hidden;
}
.tk.is-saved .tk__num--one {
visibility: visible;
}
.tk__srNum {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
margin: 0;
}
@keyframes tkFloat {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-4px);
}
}
.tk.is-demo .tk__btn {
animation: tkPress 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes tkPress {
0%, 40% {
transform: scale(1);
}
43% {
transform: scale(0.9);
}
47%, 100% {
transform: scale(1);
}
}
.tk.is-demo .tk__btnLabel--save {
animation: tkCutSave 2s steps(1, end) infinite;
}
@keyframes tkCutSave {
0%, 45% {
visibility: visible;
}
45.5%, 89% {
visibility: hidden;
}
89.5%, 100% {
visibility: visible;
}
}
.tk.is-demo .tk__btnLabel--done {
animation: tkCutDone 2s steps(1, end) infinite;
}
@keyframes tkCutDone {
0%, 45% {
visibility: hidden;
}
45.5%, 89% {
visibility: visible;
}
89.5%, 100% {
visibility: hidden;
}
}
.tk.is-demo .tk__num--zero {
animation: tkCutZero 2s steps(1, end) infinite;
}
@keyframes tkCutZero {
0%, 54% {
visibility: visible;
}
54.5%, 89% {
visibility: hidden;
}
89.5%, 100% {
visibility: visible;
}
}
.tk.is-demo .tk__num--one {
animation: tkCutOne 2s steps(1, end) infinite;
}
@keyframes tkCutOne {
0%, 54% {
visibility: hidden;
}
54.5%, 89% {
visibility: visible;
}
89.5%, 100% {
visibility: hidden;
}
}
@media (max-width: 340px) {
.tk__main {
padding: 8px 12px;
}
.tk__stub {
padding: 8px 4px;
}
}
@media (prefers-reduced-motion: reduce) {
.tk.is-demo .tk__card,
.tk.is-demo .tk__btn,
.tk.is-demo .tk__btnLabel--save,
.tk.is-demo .tk__btnLabel--done,
.tk.is-demo .tk__num--zero,
.tk.is-demo .tk__num--one {
animation: none !important;
}
.tk__btn {
transition: none !important;
}
}var tk = document.getElementById('tk');
var saveBtn = document.getElementById('saveBtn');
var srNum = document.getElementById('srNum');
saveBtn.addEventListener('click', function () {
tk.classList.remove('is-demo');
var saved = tk.classList.toggle('is-saved');
saveBtn.setAttribute('aria-pressed', saved ? 'true' : 'false');
srNum.textContent = saved ? '1 coupon' : '0 coupons';
});// TicketNotchCoupon — vanilla 판(run/283/demo/01-coupon-ticket-notch/index.html) 의 React 포트.
// 반원 노치는 mask + radial-gradient 로 진짜로 파고, 재단선은 점선 border.
// 담기를 누르면 미리보기 루프가 꺼지고 보관함 장수가 0↔1 로 바뀐다(vanilla 와 같은 동작).
import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./TicketNotchCoupon.module.scss";
export type TicketNotchCouponProps = {
/** 격자 미리보기용 2초 자동 루프. 사람이 손대면 꺼진다 */
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function TicketNotchCoupon({
demo = true,
duration = 2000,
easing = "cubic-bezier(.2, 1.4, .4, 1)",
color = "#2f6df6",
}: TicketNotchCouponProps) {
const [live, setLive] = useState(demo);
const [saved, setSaved] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
// vanilla 의 click 핸들러와 같은 순서: 미리보기 루프를 떼고 보관 상태를 뒤집는다
function applySave() {
setLive(false);
setSaved((prev: boolean) => !prev);
}
const cls = [styles.root, live ? styles.isDemo : "", saved ? styles.isSaved : ""].filter(Boolean).join(" ");
return (
<main className={styles.stage} style={vars}>
<div className={cls}>
<div className={styles.card} aria-label="첫 구매 30% 티켓 쿠폰">
<div className={styles.main}>
<p className={styles.brand}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" className={styles.ico} aria-hidden="true">
<path d="M200,128a32,32,0,0,0,32,32v32a8,8,0,0,1-8,8H96V56H224a8,8,0,0,1,8,8V96A32,32,0,0,0,200,128Z" opacity="0.2" />
<path d="M232,104a8,8,0,0,0,8-8V64a16,16,0,0,0-16-16H32A16,16,0,0,0,16,64V96a8,8,0,0,0,8,8,24,24,0,0,1,0,48,8,8,0,0,0-8,8v32a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V160a8,8,0,0,0-8-8,24,24,0,0,1,0-48ZM32,167.2a40,40,0,0,0,0-78.4V64H88V192H32Zm192,0V192H104V64H224V88.8a40,40,0,0,0,0,78.4Z" />
</svg>
<span>WELCOME</span>
</p>
<p className={styles.pct}>30<i>%</i></p>
<p className={styles.label}>첫 구매 할인</p>
<p className={styles.meta}>오늘 자정까지 · 코드 WELCOME30</p>
</div>
<div className={styles.stub}>
<button
type="button"
className={styles.btn}
aria-pressed={saved ? "true" : "false"}
aria-label="쿠폰 보관함에 담기"
onClick={applySave}
>
<span className={`${styles.btnLabel} ${styles.btnLabelSave}`}>담기</span>
<span className={`${styles.btnLabel} ${styles.btnLabelDone}`} aria-hidden="true">
<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>
</button>
</div>
</div>
<p className={styles.wallet} aria-live="polite">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" className={styles.ico} aria-hidden="true">
<path d="M224,80V192a8,8,0,0,1-8,8H56a16,16,0,0,1-16-16V56A16,16,0,0,0,56,72H216A8,8,0,0,1,224,80Z" opacity="0.2" />
<path d="M216,64H56a8,8,0,0,1,0-16H192a8,8,0,0,0,0-16H56A24,24,0,0,0,32,56V184a24,24,0,0,0,24,24H216a16,16,0,0,0,16-16V80A16,16,0,0,0,216,64Zm0,128H56a8,8,0,0,1-8-8V78.63A23.84,23.84,0,0,0,56,80H216Zm-48-60a12,12,0,1,1,12,12A12,12,0,0,1,168,132Z" />
</svg>
<span>보관함</span>
<span className={styles.count}>
<b className={`${styles.num} ${styles.numZero}`} aria-hidden="true">0</b>
<b className={`${styles.num} ${styles.numOne}`} aria-hidden="true">1</b>
<span className={styles.srNum}>{saved ? "1장" : "0장"}</span>
</span>
</p>
</div>
</main>
);
}01 Notched ticket coupon — Measured area 5.972% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · Coupon Design: 9 CSS Coupons, Stamp to Spin
The post this demo lives in
Coupon Design: 9 CSS Coupons, Stamp to SpinCoupon design is what a discount looks like on screen and how it behaves — notch, tear line, stamp slot, countdown. Thes See all demos in this post


