20 CSS Ribbons01 / 20
Corner Sale Ribbon for Product Cards
The corner ribbon every storefront ships: a 45° discount band flush across the top-right of a product card, clipped perfectly to the card's rounded corner. The geometry is parametric rather than hardcoded — one --rib-01-size value drives the corner box, the band length and the offset, so the same ribbon works on a 200px thumbnail and a 600px hero. A one-shot sheen sweeps the band on load and replays on hover.
Published
The code
<section class="rib-01" aria-label="Corner sale ribbon demo">
<div class="rib-01__stage">
<article class="rib-01__card">
<div class="rib-01__media">
<img class="rib-01__img" alt="Pale grey and blue running shoe photographed on a light neutral backdrop" loading="lazy" decoding="async" width="640" height="480"
src="https://images.unsplash.com/photo-1560769629-975ec94e6a86?q=80&w=800&auto=format&fit=crop">
</div>
<div class="rib-01__ribbon" id="rib-01-offer" role="img" aria-label="50 percent off this week">
<span class="rib-01__band"><span class="rib-01__label">50% OFF</span></span>
</div>
<div class="rib-01__body">
<p class="rib-01__brand">Meridian Athletics</p>
<h3 class="rib-01__name" aria-describedby="rib-01-offer">Trailform GT Runner</h3>
<p class="rib-01__price"><span class="rib-01__now">$89</span><s class="rib-01__was">$178</s><span class="rib-01__save">You save $89</span></p>
<button class="rib-01__cta" type="button">Add to bag</button>
<p class="rib-01__ship">Free returns · Ships tomorrow</p>
</div>
</article>
<p class="rib-01__chip" aria-hidden="true">overflow clip · rotate: 45deg · S × √2 band · clip-path: inset(round)</p>
</div>
</section><section class="rib-01" aria-label="Corner sale ribbon demo">
<div class="rib-01__stage">
<article class="rib-01__card">
<div class="rib-01__media">
<img class="rib-01__img" alt="Pale grey and blue running shoe photographed on a light neutral backdrop" loading="lazy" decoding="async" width="640" height="480"
src="https://images.unsplash.com/photo-1560769629-975ec94e6a86?q=80&w=800&auto=format&fit=crop">
</div>
<div class="rib-01__ribbon" id="rib-01-offer" role="img" aria-label="50 percent off this week">
<span class="rib-01__band"><span class="rib-01__label">50% OFF</span></span>
</div>
<div class="rib-01__body">
<p class="rib-01__brand">Meridian Athletics</p>
<h3 class="rib-01__name" aria-describedby="rib-01-offer">Trailform GT Runner</h3>
<p class="rib-01__price"><span class="rib-01__now">$89</span><s class="rib-01__was">$178</s><span class="rib-01__save">You save $89</span></p>
<button class="rib-01__cta" type="button">Add to bag</button>
<p class="rib-01__ship">Free returns · Ships tomorrow</p>
</div>
</article>
<p class="rib-01__chip" aria-hidden="true">overflow clip · rotate: 45deg · S × √2 band · clip-path: inset(round)</p>
</div>
</section>.rib-01,
.rib-01 *,
.rib-01 *::before,
.rib-01 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.rib-01 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
--rib-01-bg: #f2f0ec;
--rib-01-bg: oklch(0.96 0.006 85);
--rib-01-card: #fff;
--rib-01-card: oklch(1 0 0);
--rib-01-ink: #231f1d;
--rib-01-ink: oklch(0.24 0.012 60);
--rib-01-mut: #6f6862;
--rib-01-mut: oklch(0.56 0.014 60);
--rib-01-acc: #d8412f;
--rib-01-acc: oklch(0.58 0.21 27);
--rib-01-size: clamp(104px,26vw,148px);
--rib-01-band: 38px;
--rib-01-offset: 30px;
--rib-01-len: calc(var(--rib-01-size) * 1.415 + var(--rib-01-band) * 2);
--rib-01-r: 26px;
background: var(--rib-01-bg);
color: var(--rib-01-ink);
font-family: system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
-webkit-font-smoothing: antialiased;
}
.rib-01__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: clamp(16px,3vw,26px);
padding: clamp(18px,5vw,56px);
background: radial-gradient(120% 90% at 50% 0%,color-mix(in oklch,var(--rib-01-acc) 9%,transparent),transparent 62%);
}
.rib-01__card {
position: relative;
width: min(100%,380px);
background: var(--rib-01-card);
border-radius: var(--rib-01-r);
clip-path: inset(0 round var(--rib-01-r));
box-shadow: 0 1px 2px oklch(0 0 0/.05),0 22px 48px -18px oklch(0.35 0.03 60/.4);
transition: box-shadow .45s cubic-bezier(.16,1,.3,1),translate .45s cubic-bezier(.16,1,.3,1);
}
.rib-01__card:hover {
translate: 0 -4px;
box-shadow: 0 1px 2px oklch(0 0 0/.05),0 34px 64px -20px oklch(0.35 0.06 27/.42);
}
.rib-01__media {
aspect-ratio: 4/3;
background: linear-gradient(140deg,oklch(0.9 0.03 70),oklch(0.83 0.05 40));
}
.rib-01__img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.rib-01__ribbon {
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
inline-size: var(--rib-01-size);
aspect-ratio: 1;
overflow: hidden;
pointer-events: none;
z-index: 2;
filter: drop-shadow(0 5px 12px color-mix(in oklch,var(--rib-01-acc) 42%,transparent)) drop-shadow(0 1px 2px oklch(0 0 0/.22));
}
.rib-01__band {
position: absolute;
inset-block-start: var(--rib-01-offset);
inset-inline-end: calc((var(--rib-01-len) - var(--rib-01-size)) / -2);
inline-size: var(--rib-01-len);
min-block-size: var(--rib-01-band);
display: grid;
place-items: center;
overflow: hidden;
rotate: 45deg;
background: linear-gradient(180deg,color-mix(in oklch,var(--rib-01-acc) 88%,white 12%),var(--rib-01-acc));
box-shadow: inset 0 -1px 0 oklch(0 0 0/.18);
}
.rib-01__label {
padding: 9px 14px;
color: oklch(0.99 0 0);
font-size: clamp(12px,3vw,14px);
font-weight: 800;
letter-spacing: .1em;
line-height: 1;
white-space: nowrap;
text-shadow: 0 1px 2px oklch(0 0 0/.28);
}
.rib-01__band::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(105deg,transparent 30%,oklch(1 0 0/.55) 48%,transparent 62%);
translate: -120% 0;
animation: rib-01-sheen 1.5s cubic-bezier(.16,1,.3,1) .45s both;
}
.rib-01__card:hover .rib-01__band::after {
animation: rib-01-sheen 1.1s cubic-bezier(.16,1,.3,1) both;
}
@keyframes rib-01-sheen {
from {
translate: -120% 0;
}
to {
translate: 120% 0;
}
}
.rib-01__body {
display: grid;
gap: 7px;
padding: clamp(18px,4vw,24px);
}
.rib-01__brand {
font-size: 11.5px;
font-weight: 700;
letter-spacing: .15em;
text-transform: uppercase;
color: var(--rib-01-mut);
}
.rib-01__name {
font-size: clamp(19px,4vw,22px);
font-weight: 640;
letter-spacing: -.022em;
text-wrap: balance;
}
.rib-01__price {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 9px;
margin-top: 2px;
font-variant-numeric: tabular-nums;
}
.rib-01__now {
font-size: 24px;
font-weight: 750;
letter-spacing: -.03em;
}
.rib-01__was {
font-size: 15px;
color: var(--rib-01-mut);
text-decoration-thickness: 1.5px;
}
.rib-01__save {
font-size: 12px;
font-weight: 700;
padding: 4px 9px;
border-radius: 99px;
color: color-mix(in oklch,var(--rib-01-acc) 72%,black);
background: color-mix(in oklch,var(--rib-01-acc) 13%,transparent);
}
.rib-01__cta {
margin-top: 10px;
min-height: 48px;
border: 0;
border-radius: 14px;
background: var(--rib-01-ink);
color: oklch(0.99 0 0);
font: inherit;
font-size: 15px;
font-weight: 660;
cursor: pointer;
transition: background .25s,translate .2s;
}
.rib-01__cta:hover {
background: color-mix(in oklch,var(--rib-01-ink) 82%,var(--rib-01-acc));
translate: 0 -1px;
}
.rib-01__cta:active {
translate: 0 0;
scale: .99;
}
.rib-01__cta:focus-visible {
outline: 3px solid var(--rib-01-acc);
outline-offset: 3px;
}
.rib-01__ship {
font-size: 12px;
color: var(--rib-01-mut);
}
.rib-01__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--rib-01-mut);
padding: 8px 15px;
border: 1px solid oklch(0 0 0/.12);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.rib-01 * {
animation: none !important;
transition-duration: .01ms !important;
}
.rib-01__band::after {
display: none;
}
}.rib-01,
.rib-01 *,
.rib-01 *::before,
.rib-01 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.rib-01 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
--rib-01-bg: #f2f0ec;
--rib-01-bg: oklch(0.96 0.006 85);
--rib-01-card: #fff;
--rib-01-card: oklch(1 0 0);
--rib-01-ink: #231f1d;
--rib-01-ink: oklch(0.24 0.012 60);
--rib-01-mut: #6f6862;
--rib-01-mut: oklch(0.56 0.014 60);
--rib-01-acc: #d8412f;
--rib-01-acc: oklch(0.58 0.21 27);
--rib-01-size: clamp(104px,26vw,148px);
--rib-01-band: 38px;
--rib-01-offset: 30px;
--rib-01-len: calc(var(--rib-01-size) * 1.415 + var(--rib-01-band) * 2);
--rib-01-r: 26px;
background: var(--rib-01-bg);
color: var(--rib-01-ink);
font-family: system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
-webkit-font-smoothing: antialiased;
}
.rib-01__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: clamp(16px,3vw,26px);
padding: clamp(18px,5vw,56px);
background: radial-gradient(120% 90% at 50% 0%,color-mix(in oklch,var(--rib-01-acc) 9%,transparent),transparent 62%);
}
.rib-01__card {
position: relative;
width: min(100%,380px);
background: var(--rib-01-card);
border-radius: var(--rib-01-r);
clip-path: inset(0 round var(--rib-01-r));
box-shadow: 0 1px 2px oklch(0 0 0/.05),0 22px 48px -18px oklch(0.35 0.03 60/.4);
transition: box-shadow .45s cubic-bezier(.16,1,.3,1),translate .45s cubic-bezier(.16,1,.3,1);
}
.rib-01__card:hover {
translate: 0 -4px;
box-shadow: 0 1px 2px oklch(0 0 0/.05),0 34px 64px -20px oklch(0.35 0.06 27/.42);
}
.rib-01__media {
aspect-ratio: 4/3;
background: linear-gradient(140deg,oklch(0.9 0.03 70),oklch(0.83 0.05 40));
}
.rib-01__img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.rib-01__ribbon {
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
inline-size: var(--rib-01-size);
aspect-ratio: 1;
overflow: hidden;
pointer-events: none;
z-index: 2;
filter: drop-shadow(0 5px 12px color-mix(in oklch,var(--rib-01-acc) 42%,transparent)) drop-shadow(0 1px 2px oklch(0 0 0/.22));
}
.rib-01__band {
position: absolute;
inset-block-start: var(--rib-01-offset);
inset-inline-end: calc((var(--rib-01-len) - var(--rib-01-size)) / -2);
inline-size: var(--rib-01-len);
min-block-size: var(--rib-01-band);
display: grid;
place-items: center;
overflow: hidden;
rotate: 45deg;
background: linear-gradient(180deg,color-mix(in oklch,var(--rib-01-acc) 88%,white 12%),var(--rib-01-acc));
box-shadow: inset 0 -1px 0 oklch(0 0 0/.18);
}
.rib-01__label {
padding: 9px 14px;
color: oklch(0.99 0 0);
font-size: clamp(12px,3vw,14px);
font-weight: 800;
letter-spacing: .1em;
line-height: 1;
white-space: nowrap;
text-shadow: 0 1px 2px oklch(0 0 0/.28);
}
.rib-01__band::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(105deg,transparent 30%,oklch(1 0 0/.55) 48%,transparent 62%);
translate: -120% 0;
animation: rib-01-sheen 1.5s cubic-bezier(.16,1,.3,1) .45s both;
}
.rib-01__card:hover .rib-01__band::after {
animation: rib-01-sheen 1.1s cubic-bezier(.16,1,.3,1) both;
}
@keyframes rib-01-sheen {
from {
translate: -120% 0;
}
to {
translate: 120% 0;
}
}
.rib-01__body {
display: grid;
gap: 7px;
padding: clamp(18px,4vw,24px);
}
.rib-01__brand {
font-size: 11.5px;
font-weight: 700;
letter-spacing: .15em;
text-transform: uppercase;
color: var(--rib-01-mut);
}
.rib-01__name {
font-size: clamp(19px,4vw,22px);
font-weight: 640;
letter-spacing: -.022em;
text-wrap: balance;
}
.rib-01__price {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 9px;
margin-top: 2px;
font-variant-numeric: tabular-nums;
}
.rib-01__now {
font-size: 24px;
font-weight: 750;
letter-spacing: -.03em;
}
.rib-01__was {
font-size: 15px;
color: var(--rib-01-mut);
text-decoration-thickness: 1.5px;
}
.rib-01__save {
font-size: 12px;
font-weight: 700;
padding: 4px 9px;
border-radius: 99px;
color: color-mix(in oklch,var(--rib-01-acc) 72%,black);
background: color-mix(in oklch,var(--rib-01-acc) 13%,transparent);
}
.rib-01__cta {
margin-top: 10px;
min-height: 48px;
border: 0;
border-radius: 14px;
background: var(--rib-01-ink);
color: oklch(0.99 0 0);
font: inherit;
font-size: 15px;
font-weight: 660;
cursor: pointer;
transition: background .25s,translate .2s;
}
.rib-01__cta:hover {
background: color-mix(in oklch,var(--rib-01-ink) 82%,var(--rib-01-acc));
translate: 0 -1px;
}
.rib-01__cta:active {
translate: 0 0;
scale: .99;
}
.rib-01__cta:focus-visible {
outline: 3px solid var(--rib-01-acc);
outline-offset: 3px;
}
.rib-01__ship {
font-size: 12px;
color: var(--rib-01-mut);
}
.rib-01__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--rib-01-mut);
padding: 8px 15px;
border: 1px solid oklch(0 0 0/.12);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.rib-01 * {
animation: none !important;
transition-duration: .01ms !important;
}
.rib-01__band::after {
display: none;
}
}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: Corner Sale Ribbon for Product Cards
Source: https://codefronts.com/snippets/css-ribbons/corner-sale-ribbon-for-product-cards/
The corner ribbon every storefront ships: a 45° discount band flush across the top-right of a product card, clipped perfectly to the card's rounded corner. The geometry is parametric rather than hardcoded — one --rib-01-size value drives the corner box, the band length and the offset, so the same ribbon works on a 200px thumbnail and a 600px hero. A one-shot sheen sweeps the band on load and replays on hover.
## HTML
```html
<section class="rib-01" aria-label="Corner sale ribbon demo">
<div class="rib-01__stage">
<article class="rib-01__card">
<div class="rib-01__media">
<img class="rib-01__img" alt="Pale grey and blue running shoe photographed on a light neutral backdrop" loading="lazy" decoding="async" width="640" height="480"
src="https://images.unsplash.com/photo-1560769629-975ec94e6a86?q=80&w=800&auto=format&fit=crop">
</div>
<div class="rib-01__ribbon" id="rib-01-offer" role="img" aria-label="50 percent off this week">
<span class="rib-01__band"><span class="rib-01__label">50% OFF</span></span>
</div>
<div class="rib-01__body">
<p class="rib-01__brand">Meridian Athletics</p>
<h3 class="rib-01__name" aria-describedby="rib-01-offer">Trailform GT Runner</h3>
<p class="rib-01__price"><span class="rib-01__now">$89</span><s class="rib-01__was">$178</s><span class="rib-01__save">You save $89</span></p>
<button class="rib-01__cta" type="button">Add to bag</button>
<p class="rib-01__ship">Free returns · Ships tomorrow</p>
</div>
</article>
<p class="rib-01__chip" aria-hidden="true">overflow clip · rotate: 45deg · S × √2 band · clip-path: inset(round)</p>
</div>
</section>
```
## CSS
```css
.rib-01,
.rib-01 *,
.rib-01 *::before,
.rib-01 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.rib-01 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
--rib-01-bg: #f2f0ec;
--rib-01-bg: oklch(0.96 0.006 85);
--rib-01-card: #fff;
--rib-01-card: oklch(1 0 0);
--rib-01-ink: #231f1d;
--rib-01-ink: oklch(0.24 0.012 60);
--rib-01-mut: #6f6862;
--rib-01-mut: oklch(0.56 0.014 60);
--rib-01-acc: #d8412f;
--rib-01-acc: oklch(0.58 0.21 27);
--rib-01-size: clamp(104px,26vw,148px);
--rib-01-band: 38px;
--rib-01-offset: 30px;
--rib-01-len: calc(var(--rib-01-size) * 1.415 + var(--rib-01-band) * 2);
--rib-01-r: 26px;
background: var(--rib-01-bg);
color: var(--rib-01-ink);
font-family: system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
-webkit-font-smoothing: antialiased;
}
.rib-01__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: clamp(16px,3vw,26px);
padding: clamp(18px,5vw,56px);
background: radial-gradient(120% 90% at 50% 0%,color-mix(in oklch,var(--rib-01-acc) 9%,transparent),transparent 62%);
}
.rib-01__card {
position: relative;
width: min(100%,380px);
background: var(--rib-01-card);
border-radius: var(--rib-01-r);
clip-path: inset(0 round var(--rib-01-r));
box-shadow: 0 1px 2px oklch(0 0 0/.05),0 22px 48px -18px oklch(0.35 0.03 60/.4);
transition: box-shadow .45s cubic-bezier(.16,1,.3,1),translate .45s cubic-bezier(.16,1,.3,1);
}
.rib-01__card:hover {
translate: 0 -4px;
box-shadow: 0 1px 2px oklch(0 0 0/.05),0 34px 64px -20px oklch(0.35 0.06 27/.42);
}
.rib-01__media {
aspect-ratio: 4/3;
background: linear-gradient(140deg,oklch(0.9 0.03 70),oklch(0.83 0.05 40));
}
.rib-01__img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.rib-01__ribbon {
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
inline-size: var(--rib-01-size);
aspect-ratio: 1;
overflow: hidden;
pointer-events: none;
z-index: 2;
filter: drop-shadow(0 5px 12px color-mix(in oklch,var(--rib-01-acc) 42%,transparent)) drop-shadow(0 1px 2px oklch(0 0 0/.22));
}
.rib-01__band {
position: absolute;
inset-block-start: var(--rib-01-offset);
inset-inline-end: calc((var(--rib-01-len) - var(--rib-01-size)) / -2);
inline-size: var(--rib-01-len);
min-block-size: var(--rib-01-band);
display: grid;
place-items: center;
overflow: hidden;
rotate: 45deg;
background: linear-gradient(180deg,color-mix(in oklch,var(--rib-01-acc) 88%,white 12%),var(--rib-01-acc));
box-shadow: inset 0 -1px 0 oklch(0 0 0/.18);
}
.rib-01__label {
padding: 9px 14px;
color: oklch(0.99 0 0);
font-size: clamp(12px,3vw,14px);
font-weight: 800;
letter-spacing: .1em;
line-height: 1;
white-space: nowrap;
text-shadow: 0 1px 2px oklch(0 0 0/.28);
}
.rib-01__band::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(105deg,transparent 30%,oklch(1 0 0/.55) 48%,transparent 62%);
translate: -120% 0;
animation: rib-01-sheen 1.5s cubic-bezier(.16,1,.3,1) .45s both;
}
.rib-01__card:hover .rib-01__band::after {
animation: rib-01-sheen 1.1s cubic-bezier(.16,1,.3,1) both;
}
@keyframes rib-01-sheen {
from {
translate: -120% 0;
}
to {
translate: 120% 0;
}
}
.rib-01__body {
display: grid;
gap: 7px;
padding: clamp(18px,4vw,24px);
}
.rib-01__brand {
font-size: 11.5px;
font-weight: 700;
letter-spacing: .15em;
text-transform: uppercase;
color: var(--rib-01-mut);
}
.rib-01__name {
font-size: clamp(19px,4vw,22px);
font-weight: 640;
letter-spacing: -.022em;
text-wrap: balance;
}
.rib-01__price {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 9px;
margin-top: 2px;
font-variant-numeric: tabular-nums;
}
.rib-01__now {
font-size: 24px;
font-weight: 750;
letter-spacing: -.03em;
}
.rib-01__was {
font-size: 15px;
color: var(--rib-01-mut);
text-decoration-thickness: 1.5px;
}
.rib-01__save {
font-size: 12px;
font-weight: 700;
padding: 4px 9px;
border-radius: 99px;
color: color-mix(in oklch,var(--rib-01-acc) 72%,black);
background: color-mix(in oklch,var(--rib-01-acc) 13%,transparent);
}
.rib-01__cta {
margin-top: 10px;
min-height: 48px;
border: 0;
border-radius: 14px;
background: var(--rib-01-ink);
color: oklch(0.99 0 0);
font: inherit;
font-size: 15px;
font-weight: 660;
cursor: pointer;
transition: background .25s,translate .2s;
}
.rib-01__cta:hover {
background: color-mix(in oklch,var(--rib-01-ink) 82%,var(--rib-01-acc));
translate: 0 -1px;
}
.rib-01__cta:active {
translate: 0 0;
scale: .99;
}
.rib-01__cta:focus-visible {
outline: 3px solid var(--rib-01-acc);
outline-offset: 3px;
}
.rib-01__ship {
font-size: 12px;
color: var(--rib-01-mut);
}
.rib-01__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--rib-01-mut);
padding: 8px 15px;
border: 1px solid oklch(0 0 0/.12);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.rib-01 * {
animation: none !important;
transition-duration: .01ms !important;
}
.rib-01__band::after {
display: none;
}
}
```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: Corner Sale Ribbon for Product Cards
Source: https://codefronts.com/snippets/css-ribbons/corner-sale-ribbon-for-product-cards/
The corner ribbon every storefront ships: a 45° discount band flush across the top-right of a product card, clipped perfectly to the card's rounded corner. The geometry is parametric rather than hardcoded — one --rib-01-size value drives the corner box, the band length and the offset, so the same ribbon works on a 200px thumbnail and a 600px hero. A one-shot sheen sweeps the band on load and replays on hover.
## HTML
```html
<section class="rib-01" aria-label="Corner sale ribbon demo">
<div class="rib-01__stage">
<article class="rib-01__card">
<div class="rib-01__media">
<img class="rib-01__img" alt="Pale grey and blue running shoe photographed on a light neutral backdrop" loading="lazy" decoding="async" width="640" height="480"
src="https://images.unsplash.com/photo-1560769629-975ec94e6a86?q=80&w=800&auto=format&fit=crop">
</div>
<div class="rib-01__ribbon" id="rib-01-offer" role="img" aria-label="50 percent off this week">
<span class="rib-01__band"><span class="rib-01__label">50% OFF</span></span>
</div>
<div class="rib-01__body">
<p class="rib-01__brand">Meridian Athletics</p>
<h3 class="rib-01__name" aria-describedby="rib-01-offer">Trailform GT Runner</h3>
<p class="rib-01__price"><span class="rib-01__now">$89</span><s class="rib-01__was">$178</s><span class="rib-01__save">You save $89</span></p>
<button class="rib-01__cta" type="button">Add to bag</button>
<p class="rib-01__ship">Free returns · Ships tomorrow</p>
</div>
</article>
<p class="rib-01__chip" aria-hidden="true">overflow clip · rotate: 45deg · S × √2 band · clip-path: inset(round)</p>
</div>
</section>
```
## CSS
```css
.rib-01,
.rib-01 *,
.rib-01 *::before,
.rib-01 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.rib-01 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
--rib-01-bg: #f2f0ec;
--rib-01-bg: oklch(0.96 0.006 85);
--rib-01-card: #fff;
--rib-01-card: oklch(1 0 0);
--rib-01-ink: #231f1d;
--rib-01-ink: oklch(0.24 0.012 60);
--rib-01-mut: #6f6862;
--rib-01-mut: oklch(0.56 0.014 60);
--rib-01-acc: #d8412f;
--rib-01-acc: oklch(0.58 0.21 27);
--rib-01-size: clamp(104px,26vw,148px);
--rib-01-band: 38px;
--rib-01-offset: 30px;
--rib-01-len: calc(var(--rib-01-size) * 1.415 + var(--rib-01-band) * 2);
--rib-01-r: 26px;
background: var(--rib-01-bg);
color: var(--rib-01-ink);
font-family: system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
-webkit-font-smoothing: antialiased;
}
.rib-01__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: clamp(16px,3vw,26px);
padding: clamp(18px,5vw,56px);
background: radial-gradient(120% 90% at 50% 0%,color-mix(in oklch,var(--rib-01-acc) 9%,transparent),transparent 62%);
}
.rib-01__card {
position: relative;
width: min(100%,380px);
background: var(--rib-01-card);
border-radius: var(--rib-01-r);
clip-path: inset(0 round var(--rib-01-r));
box-shadow: 0 1px 2px oklch(0 0 0/.05),0 22px 48px -18px oklch(0.35 0.03 60/.4);
transition: box-shadow .45s cubic-bezier(.16,1,.3,1),translate .45s cubic-bezier(.16,1,.3,1);
}
.rib-01__card:hover {
translate: 0 -4px;
box-shadow: 0 1px 2px oklch(0 0 0/.05),0 34px 64px -20px oklch(0.35 0.06 27/.42);
}
.rib-01__media {
aspect-ratio: 4/3;
background: linear-gradient(140deg,oklch(0.9 0.03 70),oklch(0.83 0.05 40));
}
.rib-01__img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.rib-01__ribbon {
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
inline-size: var(--rib-01-size);
aspect-ratio: 1;
overflow: hidden;
pointer-events: none;
z-index: 2;
filter: drop-shadow(0 5px 12px color-mix(in oklch,var(--rib-01-acc) 42%,transparent)) drop-shadow(0 1px 2px oklch(0 0 0/.22));
}
.rib-01__band {
position: absolute;
inset-block-start: var(--rib-01-offset);
inset-inline-end: calc((var(--rib-01-len) - var(--rib-01-size)) / -2);
inline-size: var(--rib-01-len);
min-block-size: var(--rib-01-band);
display: grid;
place-items: center;
overflow: hidden;
rotate: 45deg;
background: linear-gradient(180deg,color-mix(in oklch,var(--rib-01-acc) 88%,white 12%),var(--rib-01-acc));
box-shadow: inset 0 -1px 0 oklch(0 0 0/.18);
}
.rib-01__label {
padding: 9px 14px;
color: oklch(0.99 0 0);
font-size: clamp(12px,3vw,14px);
font-weight: 800;
letter-spacing: .1em;
line-height: 1;
white-space: nowrap;
text-shadow: 0 1px 2px oklch(0 0 0/.28);
}
.rib-01__band::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(105deg,transparent 30%,oklch(1 0 0/.55) 48%,transparent 62%);
translate: -120% 0;
animation: rib-01-sheen 1.5s cubic-bezier(.16,1,.3,1) .45s both;
}
.rib-01__card:hover .rib-01__band::after {
animation: rib-01-sheen 1.1s cubic-bezier(.16,1,.3,1) both;
}
@keyframes rib-01-sheen {
from {
translate: -120% 0;
}
to {
translate: 120% 0;
}
}
.rib-01__body {
display: grid;
gap: 7px;
padding: clamp(18px,4vw,24px);
}
.rib-01__brand {
font-size: 11.5px;
font-weight: 700;
letter-spacing: .15em;
text-transform: uppercase;
color: var(--rib-01-mut);
}
.rib-01__name {
font-size: clamp(19px,4vw,22px);
font-weight: 640;
letter-spacing: -.022em;
text-wrap: balance;
}
.rib-01__price {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 9px;
margin-top: 2px;
font-variant-numeric: tabular-nums;
}
.rib-01__now {
font-size: 24px;
font-weight: 750;
letter-spacing: -.03em;
}
.rib-01__was {
font-size: 15px;
color: var(--rib-01-mut);
text-decoration-thickness: 1.5px;
}
.rib-01__save {
font-size: 12px;
font-weight: 700;
padding: 4px 9px;
border-radius: 99px;
color: color-mix(in oklch,var(--rib-01-acc) 72%,black);
background: color-mix(in oklch,var(--rib-01-acc) 13%,transparent);
}
.rib-01__cta {
margin-top: 10px;
min-height: 48px;
border: 0;
border-radius: 14px;
background: var(--rib-01-ink);
color: oklch(0.99 0 0);
font: inherit;
font-size: 15px;
font-weight: 660;
cursor: pointer;
transition: background .25s,translate .2s;
}
.rib-01__cta:hover {
background: color-mix(in oklch,var(--rib-01-ink) 82%,var(--rib-01-acc));
translate: 0 -1px;
}
.rib-01__cta:active {
translate: 0 0;
scale: .99;
}
.rib-01__cta:focus-visible {
outline: 3px solid var(--rib-01-acc);
outline-offset: 3px;
}
.rib-01__ship {
font-size: 12px;
color: var(--rib-01-mut);
}
.rib-01__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--rib-01-mut);
padding: 8px 15px;
border: 1px solid oklch(0 0 0/.12);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.rib-01 * {
animation: none !important;
transition-duration: .01ms !important;
}
.rib-01__band::after {
display: none;
}
}
```How this works
A 45° band across a corner is a right-triangle problem. Clip a square corner box of side S, then lay a horizontal band inside it and rotate the band 45°. Because the band now runs along the square's diagonal, it must be at least S × √2 ≈ 1.415S long to reach both edges, and centring that longer band costs half the overflow on each side:
.ribbon { /* the clipping corner box */
position: absolute; inset-block-start: 0; inset-inline-end: 0;
inline-size: var(--s); block-size: var(--s);
overflow: hidden; /* trims the band flush to the card edges */
}
.ribbon__band {
position: absolute;
--len: calc(var(--s) * 1.415 + var(--band) * 2); /* diagonal + overshoot */
inline-size: var(--len);
inset-inline-end: calc((var(--len) - var(--s)) / -2); /* centre it */
inset-block-start: var(--offset); /* slide along the diagonal */
rotate: 45deg; /* independent rotate property */
/* no box-shadow here — the corner box would clip it */
}Clipping with overflow: hidden — instead of drawing fold triangles — is what makes this read as modern rather than 2013: the band ends are cut dead flush with the card, so there is nothing to misalign when the card resizes. The card itself uses clip-path: inset(0 round var(--radius)) so the ribbon is trimmed by the rounded corner too, which plain overflow:hidden plus border-radius cannot guarantee once a filter or transform is involved.
.ribbon__band::after { /* the sheen */
content: ''; position: absolute; inset: 0;
background: linear-gradient(105deg, transparent 30%,
oklch(1 0 0 / .55) 48%, transparent 62%);
translate: -120% 0;
animation: sheen 1.5s cubic-bezier(.16,1,.3,1) .45s both;
}The band is role="img" with an aria-label spelling the offer out ("50 percent off"), and it is referenced by the product title through aria-describedby — so a screen-reader user hears the discount attached to the product, not floating loose in the page.
Make it yours
- Flip the ribbon to the top-left by swapping
inset-inline-endforinset-inline-startand usingrotate: -45deg— the ± sign is the only change, because the maths is symmetric. - Change one number to resize everything:
--rib-01-sizedrives the corner box, the diagonal and the centring offset. Scale it withclamp(104px, 26vw, 168px)— use vw or px, never a percentage (see the gotchas). - Re-theme with a single hue: every red in the demo derives from
--rib-01-accviacolor-mix(), sooklch(0.55 0.21 25)→oklch(0.55 0.21 265)turns the whole sale ribbon indigo. - Add classic folded tails by removing
overflow:hiddenand dropping two dark triangles behind the band ends — see demo 03, which does exactly that for a wrapped look.
Gotchas — read before shipping
- Rotating the band without lengthening it leaves gaps at the card edges. The band must span the diagonal, S × 1.415, not S — and because the band has thickness, its square ends get sliced by the clip box unless you overshoot the diagonal by roughly the band height at each end (S × 1.415 + 2 × band). That overshoot is free: the corner box clips it.
- A
box-shadowon the band is clipped away with the band’s overhang, so the shadow stops short of the card edges. Putfilter: drop-shadow()on the clipping corner box instead — filters are applied to the element’s already-clipped rendering, so the shadow traces the full visible band and is only trimmed by the card’s own rounding. This is the single most common broken corner-ribbon on the web. - Never put a percentage in the size token. A percentage inside a custom property is resolved lazily at each use site, so
inline-size: var(--s)resolves against width whileblock-size: var(--s)resolves against height — the corner box stops being square and the 45° maths collapses. Use px or vw, and lock the box withaspect-ratio: 1. border-radiusalone does not always clip a rotated child once the card gets a transform, filter or backdrop-filter. Useclip-path: inset(0 round R)on the card, which clips reliably in every engine.- Don't set a fixed width on the label. Localised copy ("50% RABATT", "50 % DE RÉDUCTION") is longer and will be cut off — pad the band and let it size to content.
- A ribbon over photography can vanish into the image. Give the band a shadow and enough chroma contrast against the product; check the label at 4.5:1 against the band itself, not against the page.
rotate: 45degis the modern individual transform property — it composes withtranslateandscalewithout clobbering them, unlike stacking everything into onetransform. Keep them separate so a hover translate doesn't reset the rotation.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 104+.
clip-path, individual rotate/translate properties, oklch() and color-mix() are Baseline widely available (color-mix from Chrome 111 / Safari 16.2 / Firefox 113). Older engines fall back to the plain hex declared before each oklch() value, and the ribbon geometry itself works back to IE-era transforms.