You want a bold, compact “New” label that sits on a product card without extra images, JavaScript, or bloated dependencies. In this project you will code a reusable ribbon banner for product cards using pure CSS. It will look crisp on any DPI, theme with a couple of variables, and drop into any grid of cards. You will build two variants: a flat top ribbon with folded tails and a diagonal corner ribbon. Both rely on small, elegant CSS shapes and careful positioning.
Why a Ribbon Banner Matters
Badges carry weight in product discovery. A small “New” marker drives attention without shouting. A pure CSS ribbon renders cleanly at any scale, avoids HTTP requests for images, and adapts to any color system with one change to a variable. Keeping the badge in CSS also means no layout shifts from late-loading assets and no anti-aliasing surprises. With pseudo-elements you can sketch subtle folds and tails that convey depth while staying accessible and fast.
Prerequisites
If you have written a few components with pseudo-elements and custom properties, you are ready. This project favors clarity over tricks, and each part builds on standard layout rules.
- Basic HTML
- CSS custom properties
- CSS pseudo-elements (::before / ::after)
Step 1: The HTML Structure
The ribbon should not force extra wrappers or a fragile DOM. The final HTML below uses a single ribbon element that sits inside the product card. The ribbon’s text is real markup to keep it readable by assistive tech. Tails and folds are decorative and will come from pseudo-elements.
<!-- HTML -->
<main class="demo">
<article class="product-card">
<div class="ribbon"><span class="ribbon__text">New</span></div>
<figure class="product-media">
<img src="https://picsum.photos/640/420?image=1069" alt="Canvas backpack in charcoal gray">
</figure>
<div class="product-body">
<h3 class="product-title">Canvas Backpack</h3>
<p class="product-price">$59.00</p>
<button class="btn" type="button">Add to cart</button>
</div>
</article>
<article class="product-card">
<div class="ribbon ribbon--corner"><span class="ribbon__text">New</span></div>
<figure class="product-media">
<img src="https://picsum.photos/640/420?image=1039" alt="Lightweight running shoes in blue">
</figure>
<div class="product-body">
<h3 class="product-title">Aero Runner</h3>
<p class="product-price">$89.00</p>
<button class="btn" type="button">Add to cart</button>
</div>
</article>
</main>
Step 2: The Basic CSS & Styling
Start with a small design system in :root for colors and spacing. Then style the page shell and the card. The card sets position: relative so the ribbon can anchor cleanly without hacks. A modest radius and shadow keep the product image the star while making space for the badge.
/* CSS */
:root{
--bg: hsl(210 20% 98%);
--card-bg: hsl(0 0% 100%);
--text: hsl(222 22% 10%);
--muted: hsl(222 10% 45%);
--accent: hsl(12 84% 56%);
--accent-dark: hsl(12 84% 40%);
--accent-ink: hsl(0 0% 100%);
--radius: 14px;
--shadow: 0 10px 30px hsl(0 0% 0% / 0.08);
}
*,
*::before,
*::after { box-sizing: border-box; }
body{
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
color: var(--text);
background: radial-gradient(1200px 800px at 20% 0%, hsl(210 40% 96%), var(--bg));
}
.demo{
max-width: 1100px;
margin: 4rem auto;
padding: 0 1rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
}
.product-card{
position: relative;
background: var(--card-bg);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.product-media{
margin: 0;
aspect-ratio: 16 / 10;
background: hsl(210 20% 94%);
}
.product-media img{
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.product-body{
padding: 1rem 1.25rem 1.25rem;
}
.product-title{
font-size: 1rem;
margin: 0 0 .25rem 0;
}
.product-price{
margin: 0 0 .75rem 0;
color: var(--muted);
font-weight: 600;
}
.btn{
display: inline-block;
padding: .6rem 1rem;
border-radius: 10px;
border: none;
background: hsl(222 100% 55%);
color: white;
font-weight: 650;
cursor: pointer;
transition: filter .2s;
}
.btn:hover{ filter: brightness(1.05); }
Advanced Tip: Keep ribbon colors in variables. You can swap the badge from orange to green or purple by overriding –accent and –accent-dark on a parent container, which allows quick theming of “Sale,” “New,” or “Limited” without touching the component’s structure.
Step 3: Building the Ribbon Banner
This is the flat top ribbon with tiny folded tails. It is compact, readable, and sits away from the card’s corners to avoid rounded radius clipping. The tails are made with border-based triangles, which keeps the markup lean.
/* CSS */
.ribbon{
position: absolute;
top: 12px;
left: 12px;
z-index: 2;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--accent);
color: var(--accent-ink);
font-weight: 800;
font-size: .78rem;
letter-spacing: .06em;
text-transform: uppercase;
padding: .35rem .7rem;
border-radius: .35rem;
box-shadow: 0 6px 16px hsl(12 84% 30% / .25), 0 2px 4px hsl(0 0% 0% / .12);
pointer-events: none; /* ribbon is decorative; do not block clicks beneath */
}
/* The subtle folded tails */
.ribbon::before,
.ribbon::after{
content: "";
position: absolute;
bottom: -6px;
width: 0;
height: 0;
border-top: 6px solid var(--accent-dark);
}
.ribbon::before{
left: 0;
border-left: 6px solid transparent; /* Left triangle tail */
}
.ribbon::after{
right: 0;
border-right: 6px solid transparent; /* Right triangle tail */
}
/* Text wrapper for control if you need custom spacing later */
.ribbon__text{ line-height: 1; }
How This Works (Code Breakdown)
The ribbon is absolutely positioned relative to the product card. That is why the card sets position: relative earlier. Keeping top and left small reinforces a tidy badge that does not fight for space with the image’s focal point. The ribbon uses an inline-flex wrapper to trim extra line height and center the text precisely. Pointer-events: none prevents accidental blocking of hover or click events on content below.
The folded tails use border triangles. Each pseudo-element draws a right triangle by setting a thick border on one side and transparent on the adjacent side. The left tail uses border-left transparent and border-top colored; the right tail mirrors with border-right transparent. If you want a refresher on this pattern, see how to make a triangle with CSS borders and adapt the direction you need. The badge body is a rounded rectangle; if you need a dedicated refresher on basic boxes and sizing, the library’s guide on a CSS rectangle covers the fundamentals that this component builds on.
Small shadows sell the depth. One soft spread shadow and one tight shadow do the job without heavy blur. The color pair uses –accent for the band and –accent-dark for the folds to hint at paper layers. If you want a standalone, larger header ribbon with longer tails and center notch, the library’s pattern for a CSS ribbon banner walks through that shape; the card version here aims for minimal footprint.
Step 4: Building the Corner Ribbon Variant
Some product grids prefer a diagonal corner variant that grabs attention while saving vertical space. This variant rotates the band by 45 degrees and anchors it at the top right. The same markup works; you only toggle a modifier class. Tail triangles are not needed here, so they are hidden for a clean endcap.
/* CSS */
.ribbon--corner{
left: auto;
right: -38px;
top: 14px;
transform: rotate(45deg);
padding: .5rem 2.5rem;
border-radius: 0; /* crisp diagonal */
box-shadow: 0 8px 20px hsl(12 84% 30% / .25), 0 2px 4px hsl(0 0% 0% / .18);
}
/* Corner variant does not need folded tails */
.ribbon--corner::before,
.ribbon--corner::after{
display: none;
}
How This Works (Code Breakdown)
Rotating the band changes its footprint, so the element shifts to the right with a negative offset. The negative right value pulls the center of the band over the corner. The border radius becomes zero so the edges read as a crisp strip. Because the element rotates, the stacking shadows need a small boost to keep the badge readable against busy images.
The same core rules apply: absolute positioning, real text content for “New,” and no extra wrappers. This is a drop-in class you can add when a design needs a more assertive badge. You can also flip it to the left corner by swapping right for left and inverting the translate offset.
Advanced Techniques: Adding Animations & Hover Effects
A gentle entrance gives the badge a bit of character when cards load. Use a short keyframe that rises into place while fading in. Add a hover micro-interaction on the card that lifts the ribbon slightly for depth. Respect user motion settings to keep it comfortable.
/* CSS */
@keyframes ribbon-in {
from { transform: translateY(-8px) scale(.98); opacity: 0; }
to { transform: translateY(0) scale(1); opacity: 1; }
}
.product-card .ribbon{
animation: ribbon-in .35s ease-out both;
}
.product-card:hover .ribbon{
filter: drop-shadow(0 6px 10px hsl(12 84% 30% / .25));
}
/* Corner variant also participates without clobbering its rotate */
.product-card .ribbon--corner{
animation: ribbon-in .35s ease-out both;
transform: rotate(45deg); /* end state matches earlier rule */
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce){
.product-card .ribbon,
.product-card .ribbon--corner{
animation: none;
}
}
Accessibility & Performance
Visual polish means little if the label is not perceivable or if it slows the page. A small, stable component helps both goals. Keep the text short and clear, avoid hiding it behind an image mask, and let the DOM carry the meaning rather than a decorative background.
Accessibility
The word “New” is text content in the DOM, not a pseudo-element, so it is read by screen readers. That is the right call because it conveys meaning, not decoration. The folded tails are purely visual; they live in pseudo-elements so they never clutter the accessibility tree. If your badge text changes programmatically, consider aria-live on the ribbon wrapper so assistive tech announces state changes. Maintain enough contrast between –accent and –accent-ink; a 4.5:1 ratio is a strong target for a small label. Respect motion preferences with the @media query shown so the entrance animation does not distract users who prefer reduced motion.
Performance
This ribbon uses no images, no gradients that force expensive painting, and minimal shadows. Border triangles render quickly since they are just borders on zero-sized elements. Transitions are off by default; the short keyframe runs once and stops, which keeps main-thread work low. The component avoids layout thrash by using absolute positioning and a single repaint for the animation. Defining colors and radii as variables avoids duplicate declarations and keeps your CSS smaller as your design system grows.
Ship a Badge That Works Everywhere
You built a compact “New” ribbon banner that fits any product card and a second diagonal version for layouts that favor corners. The component is theme-ready, accessible by default, and powered by simple CSS shapes. Use these patterns as a base for “Sale,” “Limited,” or category tags, and remix the variables to match any brand palette. Now you have a clean badge you can reuse across cards, lists, and landing sections without touching a graphics editor.