15 / 20
All 20 in 20 CSS Ribbons
- 01Corner Sale Ribbon for Product Cards
- 02Sold Out Corner Ribbon Overlay
- 03Bestseller Corner Ribbon
- 04Fork Me on GitHub Corner Ribbon
- 05Notched Corner Badge for Blog Cards
- 06Swallowtail V-Cut Ribbon Banner
- 07Skewed 3D Ribbon Header Banner
- 08Most Popular Pricing Card Ribbon
- 09Annual Savings Ribbon Badge
- 10Bookmark Flag Tag with Notched Edge
- 11Hanging Ribbon Bookmark
- 12Award Rosette Ribbon with Tails
- 13Certificate Seal Ribbon Rosette
- 143D Folded Ribbon Banner
- 15Coupon Ribbon with Tear-Off Edge
- 16Stitched Edge Ribbon
- 17Hover Unfold Animated Ribbon
- 18Slide-In Ribbon Notification
- 19Waving Flag Ribbon Animation
- 20Clip-Path Polygon Ribbon
Coupon Ribbon with Tear-Off Edge
A voucher with a real perforation: semicircular bites punched out of both edges and a dashed tear line between the stub and the body. The notches are carved with mask layers rather than covered with background-coloured circles, so the coupon sits correctly on any page colour, gradient or photograph. Copying the code is one button with proper success feedback.
Build interactive UI like this from scratch: 20 HTML, CSS & JavaScript projects
ZTM · JavaScript Web Projects · 30‑day money-back guarantee · Affiliate link
The code
<section class="rib-15" aria-label="Coupon ribbon with dashed tear off edge demo">
<div class="rib-15__stage">
<div class="rib-15__coupon">
<div class="rib-15__stub">
<span class="rib-15__pct">25<span>%</span></span>
<span class="rib-15__off">OFF</span>
</div>
<span class="rib-15__tear" aria-hidden="true"></span>
<div class="rib-15__body">
<p class="rib-15__brand">Northbound Coffee Roasters</p>
<h2 class="rib-15__title">Quarter off your first subscription box</h2>
<p class="rib-15__terms">Valid until <time datetime="2026-09-30">30 Sep 2026</time> · One use per customer · Excludes gift cards</p>
<div class="rib-15__coderow">
<code class="rib-15__code" id="rib-15-code">FIRSTPOUR25</code>
<button class="rib-15__copy" type="button" data-copy="FIRSTPOUR25">
<svg class="rib-15__ic rib-15__ic--copy" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false"><path fill="currentColor" d="M9 2h9a3 3 0 0 1 3 3v9h-2V5a1 1 0 0 0-1-1H9V2Zm-3 4h9a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V9a3 3 0 0 1 3-3Z"/></svg>
<svg class="rib-15__ic rib-15__ic--ok" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false"><path fill="currentColor" d="m9.6 16.4-4-4L4.2 13.8l5.4 5.4 10-10-1.4-1.4z"/></svg>
<span class="rib-15__copytxt">Copy code</span>
</button>
</div>
</div>
</div>
<p class="rib-15__live" role="status"></p>
<p class="rib-15__chip" aria-hidden="true">mask-composite: intersect notches · dashed border tear line · clipboard with fallback</p>
</div>
</section><section class="rib-15" aria-label="Coupon ribbon with dashed tear off edge demo">
<div class="rib-15__stage">
<div class="rib-15__coupon">
<div class="rib-15__stub">
<span class="rib-15__pct">25<span>%</span></span>
<span class="rib-15__off">OFF</span>
</div>
<span class="rib-15__tear" aria-hidden="true"></span>
<div class="rib-15__body">
<p class="rib-15__brand">Northbound Coffee Roasters</p>
<h2 class="rib-15__title">Quarter off your first subscription box</h2>
<p class="rib-15__terms">Valid until <time datetime="2026-09-30">30 Sep 2026</time> · One use per customer · Excludes gift cards</p>
<div class="rib-15__coderow">
<code class="rib-15__code" id="rib-15-code">FIRSTPOUR25</code>
<button class="rib-15__copy" type="button" data-copy="FIRSTPOUR25">
<svg class="rib-15__ic rib-15__ic--copy" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false"><path fill="currentColor" d="M9 2h9a3 3 0 0 1 3 3v9h-2V5a1 1 0 0 0-1-1H9V2Zm-3 4h9a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V9a3 3 0 0 1 3-3Z"/></svg>
<svg class="rib-15__ic rib-15__ic--ok" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false"><path fill="currentColor" d="m9.6 16.4-4-4L4.2 13.8l5.4 5.4 10-10-1.4-1.4z"/></svg>
<span class="rib-15__copytxt">Copy code</span>
</button>
</div>
</div>
</div>
<p class="rib-15__live" role="status"></p>
<p class="rib-15__chip" aria-hidden="true">mask-composite: intersect notches · dashed border tear line · clipboard with fallback</p>
</div>
</section>.rib-15,
.rib-15 *,
.rib-15 *::before,
.rib-15 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.rib-15 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
--rib-15-ink: #211c16;
--rib-15-ink: oklch(0.24 0.018 66);
--rib-15-mut: #6d6357;
--rib-15-mut: oklch(0.55 0.02 70);
--rib-15-paper: #fffaf1;
--rib-15-paper: oklch(0.99 0.014 88);
--rib-15-acc: #b4442a;
--rib-15-acc: oklch(0.55 0.17 38);
--rib-15-r: 14px;
--rib-15-x: 34%;
background: #2b2015;
background: linear-gradient(150deg,oklch(0.32 0.04 60),oklch(0.24 0.03 40));
color: var(--rib-15-ink);
font-family: system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
-webkit-font-smoothing: antialiased;
}
.rib-15__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: clamp(14px,3vw,22px);
padding: clamp(20px,5vw,56px);
background-image: radial-gradient(oklch(1 0 0/.045) 1px,transparent 1px);
background-size: 18px 18px;
}
.rib-15__coupon {
position: relative;
display: grid;
grid-template-columns: var(--rib-15-x) 1fr;
width: min(100%,640px);
border-radius: 20px;
background: var(--rib-15-paper);
filter: drop-shadow(0 24px 40px oklch(0 0 0/.5));
-webkit-mask-image: radial-gradient(circle var(--rib-15-r) at var(--rib-15-x) 0,transparent 98%,#000 100%),radial-gradient(circle var(--rib-15-r) at var(--rib-15-x) 100%,transparent 98%,#000 100%);
-webkit-mask-composite: source-in;
mask-image: radial-gradient(circle var(--rib-15-r) at var(--rib-15-x) 0,transparent 98%,#000 100%),radial-gradient(circle var(--rib-15-r) at var(--rib-15-x) 100%,transparent 98%,#000 100%);
mask-composite: intersect;
}
.rib-15__stub {
display: grid;
place-items: center;
align-content: center;
gap: 2px;
padding: clamp(20px,4vw,30px) 10px;
color: oklch(0.99 0.01 88);
background: #b4442a;
background: linear-gradient(160deg,color-mix(in oklch,var(--rib-15-acc) 100%,white 12%),var(--rib-15-acc) 60%,color-mix(in oklch,var(--rib-15-acc) 82%,black 12%));
border-radius: 20px 0 0 20px;
}
.rib-15__pct {
font-size: clamp(38px,8vw,58px);
font-weight: 800;
letter-spacing: -.05em;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.rib-15__pct span {
font-size: .46em;
font-weight: 700;
margin-inline-start: 2px;
}
.rib-15__off {
font-size: 13px;
font-weight: 800;
letter-spacing: .3em;
opacity: .9;
}
.rib-15__tear {
position: absolute;
inset-block: var(--rib-15-r);
inset-inline-start: var(--rib-15-x);
border-inline-start: 2px dashed color-mix(in oklch,var(--rib-15-ink) 24%,transparent);
pointer-events: none;
}
.rib-15__body {
display: grid;
gap: 9px;
padding: clamp(20px,4vw,30px);
}
.rib-15__brand {
font-size: 11px;
font-weight: 750;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--rib-15-acc);
}
.rib-15__title {
font-size: clamp(18px,3.4vw,24px);
font-weight: 660;
line-height: 1.22;
letter-spacing: -.025em;
text-wrap: balance;
}
.rib-15__terms {
font-size: 12px;
line-height: 1.5;
color: var(--rib-15-mut);
text-wrap: pretty;
}
.rib-15__coderow {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-block-start: 6px;
}
.rib-15__code {
flex: 1 1 150px;
display: grid;
place-items: center;
min-height: 48px;
padding-inline: 14px;
border-radius: 11px;
border: 2px dashed color-mix(in oklch,var(--rib-15-ink) 26%,transparent);
background: color-mix(in oklch,var(--rib-15-acc) 6%,transparent);
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 16px;
font-weight: 700;
letter-spacing: .12em;
user-select: all;
}
.rib-15__copy {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 48px;
padding-inline: 18px;
border: 0;
border-radius: 11px;
background: var(--rib-15-ink);
color: oklch(0.98 0.01 88);
font: inherit;
font-size: 14px;
font-weight: 640;
cursor: pointer;
transition: background .25s,translate .2s;
}
.rib-15__copy:hover {
translate: 0 -1px;
background: color-mix(in oklch,var(--rib-15-ink) 82%,var(--rib-15-acc));
}
.rib-15__copy:focus-visible {
outline: 3px solid var(--rib-15-acc);
outline-offset: 3px;
}
.rib-15__copy[data-state="copied"] {
background: oklch(0.5 0.13 152);
}
.rib-15__ic--ok,
.rib-15__copy[data-state="copied"] .rib-15__ic--copy {
display: none;
}
.rib-15__copy[data-state="copied"] .rib-15__ic--ok {
display: block;
}
.rib-15__live {
font-size: 12.5px;
color: oklch(0.85 0.02 80);
min-height: 1.2em;
}
.rib-15__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: oklch(0.8 0.02 80);
padding: 8px 15px;
border: 1px solid oklch(1 0 0/.16);
border-radius: 99px;
text-align: center;
}
@media (max-width: 520px) {
.rib-15__coupon {
grid-template-columns: 1fr;
--rib-15-x: 100%;
}
.rib-15__stub {
border-radius: 20px 20px 0 0;
}
.rib-15__tear {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.rib-15 * {
transition-duration: .01ms !important;
animation: none !important;
}
}.rib-15,
.rib-15 *,
.rib-15 *::before,
.rib-15 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.rib-15 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
--rib-15-ink: #211c16;
--rib-15-ink: oklch(0.24 0.018 66);
--rib-15-mut: #6d6357;
--rib-15-mut: oklch(0.55 0.02 70);
--rib-15-paper: #fffaf1;
--rib-15-paper: oklch(0.99 0.014 88);
--rib-15-acc: #b4442a;
--rib-15-acc: oklch(0.55 0.17 38);
--rib-15-r: 14px;
--rib-15-x: 34%;
background: #2b2015;
background: linear-gradient(150deg,oklch(0.32 0.04 60),oklch(0.24 0.03 40));
color: var(--rib-15-ink);
font-family: system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
-webkit-font-smoothing: antialiased;
}
.rib-15__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: clamp(14px,3vw,22px);
padding: clamp(20px,5vw,56px);
background-image: radial-gradient(oklch(1 0 0/.045) 1px,transparent 1px);
background-size: 18px 18px;
}
.rib-15__coupon {
position: relative;
display: grid;
grid-template-columns: var(--rib-15-x) 1fr;
width: min(100%,640px);
border-radius: 20px;
background: var(--rib-15-paper);
filter: drop-shadow(0 24px 40px oklch(0 0 0/.5));
-webkit-mask-image: radial-gradient(circle var(--rib-15-r) at var(--rib-15-x) 0,transparent 98%,#000 100%),radial-gradient(circle var(--rib-15-r) at var(--rib-15-x) 100%,transparent 98%,#000 100%);
-webkit-mask-composite: source-in;
mask-image: radial-gradient(circle var(--rib-15-r) at var(--rib-15-x) 0,transparent 98%,#000 100%),radial-gradient(circle var(--rib-15-r) at var(--rib-15-x) 100%,transparent 98%,#000 100%);
mask-composite: intersect;
}
.rib-15__stub {
display: grid;
place-items: center;
align-content: center;
gap: 2px;
padding: clamp(20px,4vw,30px) 10px;
color: oklch(0.99 0.01 88);
background: #b4442a;
background: linear-gradient(160deg,color-mix(in oklch,var(--rib-15-acc) 100%,white 12%),var(--rib-15-acc) 60%,color-mix(in oklch,var(--rib-15-acc) 82%,black 12%));
border-radius: 20px 0 0 20px;
}
.rib-15__pct {
font-size: clamp(38px,8vw,58px);
font-weight: 800;
letter-spacing: -.05em;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.rib-15__pct span {
font-size: .46em;
font-weight: 700;
margin-inline-start: 2px;
}
.rib-15__off {
font-size: 13px;
font-weight: 800;
letter-spacing: .3em;
opacity: .9;
}
.rib-15__tear {
position: absolute;
inset-block: var(--rib-15-r);
inset-inline-start: var(--rib-15-x);
border-inline-start: 2px dashed color-mix(in oklch,var(--rib-15-ink) 24%,transparent);
pointer-events: none;
}
.rib-15__body {
display: grid;
gap: 9px;
padding: clamp(20px,4vw,30px);
}
.rib-15__brand {
font-size: 11px;
font-weight: 750;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--rib-15-acc);
}
.rib-15__title {
font-size: clamp(18px,3.4vw,24px);
font-weight: 660;
line-height: 1.22;
letter-spacing: -.025em;
text-wrap: balance;
}
.rib-15__terms {
font-size: 12px;
line-height: 1.5;
color: var(--rib-15-mut);
text-wrap: pretty;
}
.rib-15__coderow {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-block-start: 6px;
}
.rib-15__code {
flex: 1 1 150px;
display: grid;
place-items: center;
min-height: 48px;
padding-inline: 14px;
border-radius: 11px;
border: 2px dashed color-mix(in oklch,var(--rib-15-ink) 26%,transparent);
background: color-mix(in oklch,var(--rib-15-acc) 6%,transparent);
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 16px;
font-weight: 700;
letter-spacing: .12em;
user-select: all;
}
.rib-15__copy {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 48px;
padding-inline: 18px;
border: 0;
border-radius: 11px;
background: var(--rib-15-ink);
color: oklch(0.98 0.01 88);
font: inherit;
font-size: 14px;
font-weight: 640;
cursor: pointer;
transition: background .25s,translate .2s;
}
.rib-15__copy:hover {
translate: 0 -1px;
background: color-mix(in oklch,var(--rib-15-ink) 82%,var(--rib-15-acc));
}
.rib-15__copy:focus-visible {
outline: 3px solid var(--rib-15-acc);
outline-offset: 3px;
}
.rib-15__copy[data-state="copied"] {
background: oklch(0.5 0.13 152);
}
.rib-15__ic--ok,
.rib-15__copy[data-state="copied"] .rib-15__ic--copy {
display: none;
}
.rib-15__copy[data-state="copied"] .rib-15__ic--ok {
display: block;
}
.rib-15__live {
font-size: 12.5px;
color: oklch(0.85 0.02 80);
min-height: 1.2em;
}
.rib-15__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: oklch(0.8 0.02 80);
padding: 8px 15px;
border: 1px solid oklch(1 0 0/.16);
border-radius: 99px;
text-align: center;
}
@media (max-width: 520px) {
.rib-15__coupon {
grid-template-columns: 1fr;
--rib-15-x: 100%;
}
.rib-15__stub {
border-radius: 20px 20px 0 0;
}
.rib-15__tear {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.rib-15 * {
transition-duration: .01ms !important;
animation: none !important;
}
}(() => {
document.querySelectorAll('.rib-15__stage').forEach((stage) => {
if (stage.dataset.wired) return;
stage.dataset.wired = '1';
const btn = stage.querySelector('.rib-15__copy');
const live = stage.querySelector('.rib-15__live');
const label = stage.querySelector('.rib-15__copytxt');
const codeEl = stage.querySelector('.rib-15__code');
let timer = 0;
const confirm = (code) => {
btn.dataset.state = 'copied';
label.textContent = 'Copied';
live.textContent = 'Discount code ' + code + ' copied to clipboard.';
clearTimeout(timer);
timer = setTimeout(() => { btn.dataset.state = ''; label.textContent = 'Copy code'; }, 1900);
};
btn.addEventListener('click', async () => {
const code = btn.dataset.copy;
try {
// Requires a secure context (https / localhost) and a user gesture.
await navigator.clipboard.writeText(code);
confirm(code);
} catch {
// Fallback: select the code so the user can copy it manually.
const range = document.createRange();
range.selectNodeContents(codeEl);
const sel = getSelection();
sel.removeAllRanges();
sel.addRange(range);
live.textContent = 'Code selected — press Ctrl or Cmd + C to copy.';
}
});
});
})();(() => {
document.querySelectorAll('.rib-15__stage').forEach((stage) => {
if (stage.dataset.wired) return;
stage.dataset.wired = '1';
const btn = stage.querySelector('.rib-15__copy');
const live = stage.querySelector('.rib-15__live');
const label = stage.querySelector('.rib-15__copytxt');
const codeEl = stage.querySelector('.rib-15__code');
let timer = 0;
const confirm = (code) => {
btn.dataset.state = 'copied';
label.textContent = 'Copied';
live.textContent = 'Discount code ' + code + ' copied to clipboard.';
clearTimeout(timer);
timer = setTimeout(() => { btn.dataset.state = ''; label.textContent = 'Copy code'; }, 1900);
};
btn.addEventListener('click', async () => {
const code = btn.dataset.copy;
try {
// Requires a secure context (https / localhost) and a user gesture.
await navigator.clipboard.writeText(code);
confirm(code);
} catch {
// Fallback: select the code so the user can copy it manually.
const range = document.createRange();
range.selectNodeContents(codeEl);
const sel = getSelection();
sel.removeAllRanges();
sel.addRange(range);
live.textContent = 'Code selected — press Ctrl or Cmd + C to copy.';
}
});
});
})();Here's a working CSS Ribbon from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Coupon Ribbon with Tear-Off Edge
Source: https://codefronts.com/snippets/css-ribbons/coupon-ribbon-with-tear-off-edge/
A voucher with a real perforation: semicircular bites punched out of both edges and a dashed tear line between the stub and the body. The notches are carved with mask layers rather than covered with background-coloured circles, so the coupon sits correctly on any page colour, gradient or photograph. Copying the code is one button with proper success feedback.
## HTML
```html
<section class="rib-15" aria-label="Coupon ribbon with dashed tear off edge demo">
<div class="rib-15__stage">
<div class="rib-15__coupon">
<div class="rib-15__stub">
<span class="rib-15__pct">25<span>%</span></span>
<span class="rib-15__off">OFF</span>
</div>
<span class="rib-15__tear" aria-hidden="true"></span>
<div class="rib-15__body">
<p class="rib-15__brand">Northbound Coffee Roasters</p>
<h2 class="rib-15__title">Quarter off your first subscription box</h2>
<p class="rib-15__terms">Valid until <time datetime="2026-09-30">30 Sep 2026</time> · One use per customer · Excludes gift cards</p>
<div class="rib-15__coderow">
<code class="rib-15__code" id="rib-15-code">FIRSTPOUR25</code>
<button class="rib-15__copy" type="button" data-copy="FIRSTPOUR25">
<svg class="rib-15__ic rib-15__ic--copy" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false"><path fill="currentColor" d="M9 2h9a3 3 0 0 1 3 3v9h-2V5a1 1 0 0 0-1-1H9V2Zm-3 4h9a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V9a3 3 0 0 1 3-3Z"/></svg>
<svg class="rib-15__ic rib-15__ic--ok" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false"><path fill="currentColor" d="m9.6 16.4-4-4L4.2 13.8l5.4 5.4 10-10-1.4-1.4z"/></svg>
<span class="rib-15__copytxt">Copy code</span>
</button>
</div>
</div>
</div>
<p class="rib-15__live" role="status"></p>
<p class="rib-15__chip" aria-hidden="true">mask-composite: intersect notches · dashed border tear line · clipboard with fallback</p>
</div>
</section>
```
## CSS
```css
.rib-15,
.rib-15 *,
.rib-15 *::before,
.rib-15 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.rib-15 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
--rib-15-ink: #211c16;
--rib-15-ink: oklch(0.24 0.018 66);
--rib-15-mut: #6d6357;
--rib-15-mut: oklch(0.55 0.02 70);
--rib-15-paper: #fffaf1;
--rib-15-paper: oklch(0.99 0.014 88);
--rib-15-acc: #b4442a;
--rib-15-acc: oklch(0.55 0.17 38);
--rib-15-r: 14px;
--rib-15-x: 34%;
background: #2b2015;
background: linear-gradient(150deg,oklch(0.32 0.04 60),oklch(0.24 0.03 40));
color: var(--rib-15-ink);
font-family: system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
-webkit-font-smoothing: antialiased;
}
.rib-15__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: clamp(14px,3vw,22px);
padding: clamp(20px,5vw,56px);
background-image: radial-gradient(oklch(1 0 0/.045) 1px,transparent 1px);
background-size: 18px 18px;
}
.rib-15__coupon {
position: relative;
display: grid;
grid-template-columns: var(--rib-15-x) 1fr;
width: min(100%,640px);
border-radius: 20px;
background: var(--rib-15-paper);
filter: drop-shadow(0 24px 40px oklch(0 0 0/.5));
-webkit-mask-image: radial-gradient(circle var(--rib-15-r) at var(--rib-15-x) 0,transparent 98%,#000 100%),radial-gradient(circle var(--rib-15-r) at var(--rib-15-x) 100%,transparent 98%,#000 100%);
-webkit-mask-composite: source-in;
mask-image: radial-gradient(circle var(--rib-15-r) at var(--rib-15-x) 0,transparent 98%,#000 100%),radial-gradient(circle var(--rib-15-r) at var(--rib-15-x) 100%,transparent 98%,#000 100%);
mask-composite: intersect;
}
.rib-15__stub {
display: grid;
place-items: center;
align-content: center;
gap: 2px;
padding: clamp(20px,4vw,30px) 10px;
color: oklch(0.99 0.01 88);
background: #b4442a;
background: linear-gradient(160deg,color-mix(in oklch,var(--rib-15-acc) 100%,white 12%),var(--rib-15-acc) 60%,color-mix(in oklch,var(--rib-15-acc) 82%,black 12%));
border-radius: 20px 0 0 20px;
}
.rib-15__pct {
font-size: clamp(38px,8vw,58px);
font-weight: 800;
letter-spacing: -.05em;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.rib-15__pct span {
font-size: .46em;
font-weight: 700;
margin-inline-start: 2px;
}
.rib-15__off {
font-size: 13px;
font-weight: 800;
letter-spacing: .3em;
opacity: .9;
}
.rib-15__tear {
position: absolute;
inset-block: var(--rib-15-r);
inset-inline-start: var(--rib-15-x);
border-inline-start: 2px dashed color-mix(in oklch,var(--rib-15-ink) 24%,transparent);
pointer-events: none;
}
.rib-15__body {
display: grid;
gap: 9px;
padding: clamp(20px,4vw,30px);
}
.rib-15__brand {
font-size: 11px;
font-weight: 750;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--rib-15-acc);
}
.rib-15__title {
font-size: clamp(18px,3.4vw,24px);
font-weight: 660;
line-height: 1.22;
letter-spacing: -.025em;
text-wrap: balance;
}
.rib-15__terms {
font-size: 12px;
line-height: 1.5;
color: var(--rib-15-mut);
text-wrap: pretty;
}
.rib-15__coderow {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-block-start: 6px;
}
.rib-15__code {
flex: 1 1 150px;
display: grid;
place-items: center;
min-height: 48px;
padding-inline: 14px;
border-radius: 11px;
border: 2px dashed color-mix(in oklch,var(--rib-15-ink) 26%,transparent);
background: color-mix(in oklch,var(--rib-15-acc) 6%,transparent);
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 16px;
font-weight: 700;
letter-spacing: .12em;
user-select: all;
}
.rib-15__copy {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 48px;
padding-inline: 18px;
border: 0;
border-radius: 11px;
background: var(--rib-15-ink);
color: oklch(0.98 0.01 88);
font: inherit;
font-size: 14px;
font-weight: 640;
cursor: pointer;
transition: background .25s,translate .2s;
}
.rib-15__copy:hover {
translate: 0 -1px;
background: color-mix(in oklch,var(--rib-15-ink) 82%,var(--rib-15-acc));
}
.rib-15__copy:focus-visible {
outline: 3px solid var(--rib-15-acc);
outline-offset: 3px;
}
.rib-15__copy[data-state="copied"] {
background: oklch(0.5 0.13 152);
}
.rib-15__ic--ok,
.rib-15__copy[data-state="copied"] .rib-15__ic--copy {
display: none;
}
.rib-15__copy[data-state="copied"] .rib-15__ic--ok {
display: block;
}
.rib-15__live {
font-size: 12.5px;
color: oklch(0.85 0.02 80);
min-height: 1.2em;
}
.rib-15__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: oklch(0.8 0.02 80);
padding: 8px 15px;
border: 1px solid oklch(1 0 0/.16);
border-radius: 99px;
text-align: center;
}
@media (max-width: 520px) {
.rib-15__coupon {
grid-template-columns: 1fr;
--rib-15-x: 100%;
}
.rib-15__stub {
border-radius: 20px 20px 0 0;
}
.rib-15__tear {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.rib-15 * {
transition-duration: .01ms !important;
animation: none !important;
}
}
```
## JavaScript
```js
(() => {
document.querySelectorAll('.rib-15__stage').forEach((stage) => {
if (stage.dataset.wired) return;
stage.dataset.wired = '1';
const btn = stage.querySelector('.rib-15__copy');
const live = stage.querySelector('.rib-15__live');
const label = stage.querySelector('.rib-15__copytxt');
const codeEl = stage.querySelector('.rib-15__code');
let timer = 0;
const confirm = (code) => {
btn.dataset.state = 'copied';
label.textContent = 'Copied';
live.textContent = 'Discount code ' + code + ' copied to clipboard.';
clearTimeout(timer);
timer = setTimeout(() => { btn.dataset.state = ''; label.textContent = 'Copy code'; }, 1900);
};
btn.addEventListener('click', async () => {
const code = btn.dataset.copy;
try {
// Requires a secure context (https / localhost) and a user gesture.
await navigator.clipboard.writeText(code);
confirm(code);
} catch {
// Fallback: select the code so the user can copy it manually.
const range = document.createRange();
range.selectNodeContents(codeEl);
const sel = getSelection();
sel.removeAllRanges();
sel.addRange(range);
live.textContent = 'Code selected — press Ctrl or Cmd + C to copy.';
}
});
});
})();
```Here's a working CSS Ribbon from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Coupon Ribbon with Tear-Off Edge
Source: https://codefronts.com/snippets/css-ribbons/coupon-ribbon-with-tear-off-edge/
A voucher with a real perforation: semicircular bites punched out of both edges and a dashed tear line between the stub and the body. The notches are carved with mask layers rather than covered with background-coloured circles, so the coupon sits correctly on any page colour, gradient or photograph. Copying the code is one button with proper success feedback.
## HTML
```html
<section class="rib-15" aria-label="Coupon ribbon with dashed tear off edge demo">
<div class="rib-15__stage">
<div class="rib-15__coupon">
<div class="rib-15__stub">
<span class="rib-15__pct">25<span>%</span></span>
<span class="rib-15__off">OFF</span>
</div>
<span class="rib-15__tear" aria-hidden="true"></span>
<div class="rib-15__body">
<p class="rib-15__brand">Northbound Coffee Roasters</p>
<h2 class="rib-15__title">Quarter off your first subscription box</h2>
<p class="rib-15__terms">Valid until <time datetime="2026-09-30">30 Sep 2026</time> · One use per customer · Excludes gift cards</p>
<div class="rib-15__coderow">
<code class="rib-15__code" id="rib-15-code">FIRSTPOUR25</code>
<button class="rib-15__copy" type="button" data-copy="FIRSTPOUR25">
<svg class="rib-15__ic rib-15__ic--copy" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false"><path fill="currentColor" d="M9 2h9a3 3 0 0 1 3 3v9h-2V5a1 1 0 0 0-1-1H9V2Zm-3 4h9a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V9a3 3 0 0 1 3-3Z"/></svg>
<svg class="rib-15__ic rib-15__ic--ok" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false"><path fill="currentColor" d="m9.6 16.4-4-4L4.2 13.8l5.4 5.4 10-10-1.4-1.4z"/></svg>
<span class="rib-15__copytxt">Copy code</span>
</button>
</div>
</div>
</div>
<p class="rib-15__live" role="status"></p>
<p class="rib-15__chip" aria-hidden="true">mask-composite: intersect notches · dashed border tear line · clipboard with fallback</p>
</div>
</section>
```
## CSS
```css
.rib-15,
.rib-15 *,
.rib-15 *::before,
.rib-15 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.rib-15 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
--rib-15-ink: #211c16;
--rib-15-ink: oklch(0.24 0.018 66);
--rib-15-mut: #6d6357;
--rib-15-mut: oklch(0.55 0.02 70);
--rib-15-paper: #fffaf1;
--rib-15-paper: oklch(0.99 0.014 88);
--rib-15-acc: #b4442a;
--rib-15-acc: oklch(0.55 0.17 38);
--rib-15-r: 14px;
--rib-15-x: 34%;
background: #2b2015;
background: linear-gradient(150deg,oklch(0.32 0.04 60),oklch(0.24 0.03 40));
color: var(--rib-15-ink);
font-family: system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
-webkit-font-smoothing: antialiased;
}
.rib-15__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: clamp(14px,3vw,22px);
padding: clamp(20px,5vw,56px);
background-image: radial-gradient(oklch(1 0 0/.045) 1px,transparent 1px);
background-size: 18px 18px;
}
.rib-15__coupon {
position: relative;
display: grid;
grid-template-columns: var(--rib-15-x) 1fr;
width: min(100%,640px);
border-radius: 20px;
background: var(--rib-15-paper);
filter: drop-shadow(0 24px 40px oklch(0 0 0/.5));
-webkit-mask-image: radial-gradient(circle var(--rib-15-r) at var(--rib-15-x) 0,transparent 98%,#000 100%),radial-gradient(circle var(--rib-15-r) at var(--rib-15-x) 100%,transparent 98%,#000 100%);
-webkit-mask-composite: source-in;
mask-image: radial-gradient(circle var(--rib-15-r) at var(--rib-15-x) 0,transparent 98%,#000 100%),radial-gradient(circle var(--rib-15-r) at var(--rib-15-x) 100%,transparent 98%,#000 100%);
mask-composite: intersect;
}
.rib-15__stub {
display: grid;
place-items: center;
align-content: center;
gap: 2px;
padding: clamp(20px,4vw,30px) 10px;
color: oklch(0.99 0.01 88);
background: #b4442a;
background: linear-gradient(160deg,color-mix(in oklch,var(--rib-15-acc) 100%,white 12%),var(--rib-15-acc) 60%,color-mix(in oklch,var(--rib-15-acc) 82%,black 12%));
border-radius: 20px 0 0 20px;
}
.rib-15__pct {
font-size: clamp(38px,8vw,58px);
font-weight: 800;
letter-spacing: -.05em;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.rib-15__pct span {
font-size: .46em;
font-weight: 700;
margin-inline-start: 2px;
}
.rib-15__off {
font-size: 13px;
font-weight: 800;
letter-spacing: .3em;
opacity: .9;
}
.rib-15__tear {
position: absolute;
inset-block: var(--rib-15-r);
inset-inline-start: var(--rib-15-x);
border-inline-start: 2px dashed color-mix(in oklch,var(--rib-15-ink) 24%,transparent);
pointer-events: none;
}
.rib-15__body {
display: grid;
gap: 9px;
padding: clamp(20px,4vw,30px);
}
.rib-15__brand {
font-size: 11px;
font-weight: 750;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--rib-15-acc);
}
.rib-15__title {
font-size: clamp(18px,3.4vw,24px);
font-weight: 660;
line-height: 1.22;
letter-spacing: -.025em;
text-wrap: balance;
}
.rib-15__terms {
font-size: 12px;
line-height: 1.5;
color: var(--rib-15-mut);
text-wrap: pretty;
}
.rib-15__coderow {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-block-start: 6px;
}
.rib-15__code {
flex: 1 1 150px;
display: grid;
place-items: center;
min-height: 48px;
padding-inline: 14px;
border-radius: 11px;
border: 2px dashed color-mix(in oklch,var(--rib-15-ink) 26%,transparent);
background: color-mix(in oklch,var(--rib-15-acc) 6%,transparent);
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 16px;
font-weight: 700;
letter-spacing: .12em;
user-select: all;
}
.rib-15__copy {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 48px;
padding-inline: 18px;
border: 0;
border-radius: 11px;
background: var(--rib-15-ink);
color: oklch(0.98 0.01 88);
font: inherit;
font-size: 14px;
font-weight: 640;
cursor: pointer;
transition: background .25s,translate .2s;
}
.rib-15__copy:hover {
translate: 0 -1px;
background: color-mix(in oklch,var(--rib-15-ink) 82%,var(--rib-15-acc));
}
.rib-15__copy:focus-visible {
outline: 3px solid var(--rib-15-acc);
outline-offset: 3px;
}
.rib-15__copy[data-state="copied"] {
background: oklch(0.5 0.13 152);
}
.rib-15__ic--ok,
.rib-15__copy[data-state="copied"] .rib-15__ic--copy {
display: none;
}
.rib-15__copy[data-state="copied"] .rib-15__ic--ok {
display: block;
}
.rib-15__live {
font-size: 12.5px;
color: oklch(0.85 0.02 80);
min-height: 1.2em;
}
.rib-15__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: oklch(0.8 0.02 80);
padding: 8px 15px;
border: 1px solid oklch(1 0 0/.16);
border-radius: 99px;
text-align: center;
}
@media (max-width: 520px) {
.rib-15__coupon {
grid-template-columns: 1fr;
--rib-15-x: 100%;
}
.rib-15__stub {
border-radius: 20px 20px 0 0;
}
.rib-15__tear {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.rib-15 * {
transition-duration: .01ms !important;
animation: none !important;
}
}
```
## JavaScript
```js
(() => {
document.querySelectorAll('.rib-15__stage').forEach((stage) => {
if (stage.dataset.wired) return;
stage.dataset.wired = '1';
const btn = stage.querySelector('.rib-15__copy');
const live = stage.querySelector('.rib-15__live');
const label = stage.querySelector('.rib-15__copytxt');
const codeEl = stage.querySelector('.rib-15__code');
let timer = 0;
const confirm = (code) => {
btn.dataset.state = 'copied';
label.textContent = 'Copied';
live.textContent = 'Discount code ' + code + ' copied to clipboard.';
clearTimeout(timer);
timer = setTimeout(() => { btn.dataset.state = ''; label.textContent = 'Copy code'; }, 1900);
};
btn.addEventListener('click', async () => {
const code = btn.dataset.copy;
try {
// Requires a secure context (https / localhost) and a user gesture.
await navigator.clipboard.writeText(code);
confirm(code);
} catch {
// Fallback: select the code so the user can copy it manually.
const range = document.createRange();
range.selectNodeContents(codeEl);
const sel = getSelection();
sel.removeAllRanges();
sel.addRange(range);
live.textContent = 'Code selected — press Ctrl or Cmd + C to copy.';
}
});
});
})();
```How this works
The classic "cover the notch with a circle the colour of the page" hack breaks the moment your background is not a flat colour. Masking removes the pixels for real:
.coupon {
--r: 13px; /* notch radius */
mask-image:
radial-gradient(circle var(--r) at var(--x) 0, transparent 98%, #000 100%),
radial-gradient(circle var(--r) at var(--x) 100%, transparent 98%, #000 100%);
mask-composite: intersect; /* keep only what BOTH layers keep */
}Each layer is opaque everywhere except one circle. intersect keeps a pixel only if every layer keeps it, so both circles get bitten out. The 98%→100% stop rather than a hard edge is deliberate: it gives the mask one pixel of anti-aliasing so the arc is not jagged.
The tear line is a dashed border on an absolutely-positioned rule, which stays crisp where a dashed background-image would blur:
.tear {
position: absolute; inset-block: var(--r); inset-inline-start: var(--x);
border-inline-start: 2px dashed color-mix(in oklch, var(--ink) 22%, transparent);
}Because the tear line and the notch centres share the same --x, the perforation always lines up — move the stub and everything follows.
await navigator.clipboard.writeText(code);
btn.dataset.state = 'copied';
live.textContent = 'Code ' + code + ' copied to clipboard';
setTimeout(() => (btn.dataset.state = ''), 1800);Clipboard writes need a user gesture and a secure context; the handler falls back to selecting the code text so the user can copy manually, and the confirmation goes to an aria-live region because a checkmark that only appears visually tells a screen-reader user nothing.
Make it yours
- Move the tear:
--rib-15-xpositions the notches and the dashed line together; 30% gives a classic stub, 50% a symmetrical two-part voucher. - Add a scalloped bottom edge with a third mask layer using a repeating radial-gradient — the composite list simply grows.
- Vertical ticket: swap the notch positions to
0 var(--y)and100% var(--y)and rotate the tear line. - Print-ready: add
@media print { .coupon { box-shadow: none; outline: 1px solid #000 } }so the cut line survives a black-and-white printer.
Gotchas — read before shipping
- Panels only stay welded together if their
transform-originsits on the shared crease. Rotate a wing about its own centre and it detaches from the face by half its width — the gap is small at 20deg and unmissable at 60deg. - Safari still needs
-webkit-mask-imageand-webkit-mask-composite: source-in(its keyword for intersect). Ship both spellings or the notches vanish. maskshorthand resetsmask-composite. Declare the longhands, or put composite after the shorthand.navigator.clipboardis undefined on http:// origins and inside sandboxed iframes without allow-same-origin. Always guard it and provide a text-selection fallback.- A visual-only "Copied!" state is inaccessible. Announce it in a polite live region and keep the button's accessible name stable.
- Coupon codes in the DOM are scraped constantly. Rate-limit redemption server-side; CSS obfuscation is not security.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 120+ | 15.4+ | 121+ | 120+ |
mask-composite is Baseline from Chrome 120 / Safari 15.4 (-webkit-) / Firefox 53. Without it the coupon renders as a plain rounded card with the dashed tear line intact — a graceful, still-usable fallback. Clipboard API requires HTTPS.