Price drives action on product cards. A clear, well-placed price tag reads faster than a paragraph and anchors the eye near the add-to-cart button. In this tutorial you will build a polished, hanging “price tag” element for an e-commerce site using only HTML and CSS. The tag will have a triangular notch, a punched hole, and a small string, and it will support hover animation, theming, and accessible semantics.
Why a CSS Price Tag Matters
A styled price tag does more than display numbers. It signals affordance, communicates status, and creates a visual target that users recognize. Building it in CSS keeps the markup light, avoids raster assets, and lets you theme it with a few variables. You gain instant control over shape, color, motion, and placement without downloading an extra SVG or image. On responsive layouts, a CSS tag scales cleanly and stays crisp across pixel densities.
Prerequisites
You only need core layout skills. The component uses variables and pseudo-elements to draw the notch, hole, and string.
- Basic HTML
- CSS custom properties
- CSS pseudo-elements (::before / ::after)
Step 1: The HTML Structure
The final HTML keeps the product card simple and isolates the tag in a small wrapper. The wrapper anchors the string, while the anchor element forms the visual tag body and the interactive hit area. The tag text holds the price. You can reuse the same structure anywhere on a grid of products.
<!-- HTML -->
<div class="product-card">
<img class="product-card__img" src="https://images.unsplash.com/photo-1542291026-7eec264c27ff?q=80&w=600&auto=format&fit=crop" alt="Black running sneakers">
<span class="price-tag-wrap">
<a class="price-tag" href="#" aria-label="Price: $39.00">
<span class="price-tag__text">$39</span>
</a>
</span>
</div>
Step 2: The Basic CSS & Styling
Start with a small design system. Variables cover background, border, text, and accent. The card sets context and gives the tag a place to hang. The wrapper positions the tag in the top-right corner of the product image.
/* CSS */
:root {
--tag-bg: #ffd34d;
--tag-border: #d4a62a;
--tag-text: #2b2b2b;
--tag-shadow: rgba(15, 15, 15, 0.15);
--string: #c8c8c8;
--focus: #0ea5e9;
--card-bg: #0f172a;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background: linear-gradient(180deg, #0b1020, #0a0f1a);
font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
color: #e5e7eb;
}
.product-card {
width: 320px;
aspect-ratio: 4 / 3;
position: relative;
overflow: hidden;
border-radius: 14px;
background: #111827;
box-shadow: 0 12px 30px rgba(0,0,0,.4);
}
.product-card__img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.price-tag-wrap {
position: absolute;
top: 14px;
right: 14px;
display: inline-block;
pointer-events: none; /* wrapper is decorative; the anchor remains clickable */
}
Advanced Tip: Using CSS variables for colors gives you instant seasonal themes (summer yellow, winter blue, clearance red) without touching the HTML. You can even swap variables per card for A/B tests or category-specific palettes.
Step 3: Building the Tag Body
The anchor forms the body of the tag. A triangle on the left creates the classic notch. A strong border, rounded corners, and a subtle shadow lift it from the image. The text uses tabular numbers for clean alignment as prices change.
/* CSS */
.price-tag {
position: relative;
display: inline-block;
pointer-events: auto; /* restore click on the anchor */
text-decoration: none;
background: var(--tag-bg);
color: var(--tag-text);
border: 2px solid var(--tag-border);
border-radius: 8px;
padding: 8px 20px 8px 16px; /* space for text and hole */
box-shadow: 0 6px 18px var(--tag-shadow);
transform-origin: 80% 50%; /* near the hole for a natural swing */
transition: transform 220ms cubic-bezier(.2,.7,.2,1), box-shadow 220ms;
}
.price-tag__text {
font-weight: 800;
letter-spacing: 0.2px;
font-feature-settings: "tnum" 1, "lnum" 1; /* tabular lining numerals */
display: inline-block;
}
/* The notch: a right-pointing triangle that bites into the tag edge */
.price-tag::after {
content: "";
position: absolute;
left: -12px; /* place the triangle outside to the left */
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-top: 12px solid transparent;
border-bottom: 12px solid transparent;
border-right: 12px solid var(--tag-bg); /* triangle fill matches tag */
filter: drop-shadow(-2px 0 0 var(--tag-border)); /* fake a border on the slanted edge */
}
/* Hover/focus micro-interaction */
.price-tag:hover,
.price-tag:focus-visible {
transform: rotate(-3deg) translateZ(0);
box-shadow: 0 10px 26px var(--tag-shadow);
}
/* Focus ring for keyboard users */
.price-tag:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 2px;
}
How This Works (Code Breakdown)
The anchor gets position: relative so its pseudo-elements attach to its box. Setting transform-origin near the right side makes rotations feel like a tag swinging from its hole. The padding reserves space for the hole and keeps numbers clear of the notch. A 2px border defines the silhouette while box-shadow adds depth against photo backgrounds.
The notch uses the border triangle trick: width and height are zero, and you draw a triangle by making two borders transparent and one border colored. The piece sits just outside the left edge and visually eats into the tag. If you want to master triangles, the guide on how to make a triangle-right with CSS breaks down the technique and sizing math.
The filter: drop-shadow adds a crisp edge along the slanted cut without creating an extra element. You can tune the offset to get a thin dark seam that reads as a border continuation. The text uses tabular numerals so $9, $10, and $100 retain the same digit width, which helps when prices change on hover or in quick views.
Step 4: Adding the Hole and String
The punched hole is a circular cutout, and the string is a soft arc that attaches above the hole. Both are decorative and do not interfere with focus or clicks. The hole sits inside the tag as a pseudo-element. The string attaches to the wrapper (which has no pointer events) so the link remains easy to activate.
/* CSS */
/* The punched hole (transparent center with a thin ring) */
.price-tag::before {
content: "";
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 12px;
height: 12px;
border-radius: 50%;
/* radial-gradient draws a donut: transparent center, ring in border color */
background: radial-gradient(circle at 50% 50%, transparent 55%, var(--tag-border) 56% 100%);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.35); /* slight highlight on the ring */
pointer-events: none;
}
/* The string: a curved quarter arc drawn with borders on a rounded box */
.price-tag-wrap::before {
content: "";
position: absolute;
right: 22px; /* lines up with the tag hole */
top: -6px;
width: 46px;
height: 46px;
border: 2px solid var(--string);
border-left: none;
border-bottom: none;
border-radius: 0 48px 0 0;
transform: rotate(28deg);
opacity: 0.9;
filter: drop-shadow(0 1px 0 rgba(0,0,0,0.25));
pointer-events: none;
}
How This Works (Code Breakdown)
The hole uses a radial-gradient to create a transparent center. This lets the product image show through, which looks more authentic than a flat circle. Positioning the hole with right and top keeps it stable if the tag width changes. If you prefer an explicit circular element, you can also build it with border-radius and background-clip, but the gradient version is compact and readable.
The string uses a classic border trick: a square with only the top and right borders visible and a large border-radius produces a quarter circle. Rotating the shape by roughly 28 degrees creates a natural hang angle, and a small drop shadow adds depth. Because the string sits on the wrapper, it never blocks clicks on the anchor. For reference on circles and rounded shapes, the tutorial on how to make a circle with CSS explains the fundamentals of perfect circular geometry in CSS.
Advanced Techniques: Animations & Hover Effects
Add a gentle swing on hover and a subtle entrance animation for first paint. Include a reduced-motion fallback. You can also provide a sale theme by swapping variables on a modifier class.
/* CSS */
/* Entrance: a slight drop and swing */
@keyframes tag-enter {
0% { transform: translateY(-10px) rotate(-8deg); opacity: 0; }
60% { transform: translateY(0) rotate(3deg); opacity: 1; }
100% { transform: translateY(0) rotate(0deg); }
}
.product-card .price-tag {
animation: tag-enter 420ms cubic-bezier(.2,.7,.2,1) both 120ms;
}
/* Hover swing amplifies slightly, anchored at the hole */
.product-card:hover .price-tag {
transform: rotate(-6deg);
}
/* Sale variant via a class that swaps variables */
.price-tag--sale {
--tag-bg: #ff6470;
--tag-border: #c13a45;
--tag-text: #fff;
}
/* Reduced motion: keep it static and rely on color/contrast */
@media (prefers-reduced-motion: reduce) {
.product-card .price-tag {
animation: none;
transition: none;
}
}
If your design includes a diagonal “SALE” ribbon across the corner of the tag, build it as its own element rather than baking it into the price anchor. A reference for that shape is the guide on how to make a ribbon banner with CSS, which pairs well with this component if you need seasonal or clearance markers.
Accessibility & Performance
Design polish should never block usability. The tag is an interactive control that users will navigate with keyboard, touch, and screen readers. Animations should not distract or cause fatigue, and the impact on rendering must stay small on grid pages with many items.
Accessibility
The anchor has an aria-label that spells out the price, which helps when the visual contains symbols or when currency formatting changes. Keep the label synchronized with the visible content in your template logic. The decorative hole and string use pseudo-elements and carry no text, which means they are ignored by assistive technology. The wrapper uses pointer-events: none to avoid creating a dead zone that traps clicks or taps.
Always provide a visible focus ring. The sample uses outline in a high-contrast blue to meet WCAG focus criteria. Confirm text contrast for your theme; yellow tags with dark text pass easily, while red tags may need white text and a thicker ring. If you animate the tag, guard it with prefers-reduced-motion and fall back to static states to respect user settings.
Performance
The component relies on cheap primitives: borders, border-radius, gradients, and transforms. These are fast to paint and composited well by browsers. Avoid animating box-shadow or filter on large lists of products. In the sample, transitions animate transform only, which uses the compositor and keeps the main thread free. Keep shadows static and modest. If you render dozens of cards, consider disabling entrance animation and only keep the micro hover effect.
From Shape to Signal
You now have a reusable, themeable price tag with a notch, hole, and string that fits neatly on any product card. The same approach scales to badges, coupons, or quick-view overlays. Tweak the variables, timing, and typography, and you have a clear price signal that travels well across your catalog.