Card UI does not have to be a rectangle with a shadow. You can turn a stock card into a branded component with a few shape accents: a corner ribbon for status, a circular avatar that breaks the frame, a coupon notch that signals deals, a price-tag tab that cues commerce, and a starburst sticker that shouts a promo. This tutorial walks through all five, built only with CSS so you can drop them in any stack without image assets.
Why CSS Shapes in Cards Matter
Shapes guide attention better than another layer of gradients or blur. They work in pure CSS, so there is no SVG pipeline, no icon font, and no extra HTTP requests. Pseudo-elements and gradients paint layers that scale to any density and theme well with CSS variables. Cards benefit most because they repeat across a grid, and small shape details make repeated content feel intentional instead of generic.
Prerequisites
You should be comfortable reading class-based CSS, positioning pseudo-elements, and using variables for theming. The examples use modern properties that ship in evergreen browsers.
- Basic HTML
- CSS custom properties
- CSS pseudo-elements (::before / ::after)
Step 1: The HTML Structure
The grid contains five cards. Each card shares a common .card shell, and then a modifier class adds its shape treatment. Only the avatar version needs a small extra child element for the circular badge. All other shapes are drawn with pseudo-elements so the HTML stays clean.
<!-- HTML -->
<section class="cards">
<article class="card card--ribbon">
<header class="card__header">Corner Ribbon</header>
<h3 class="card__title">Pro Plan</h3>
<p class="card__text">Use a corner ribbon to flag tiers, states, or featured content without adding markup.</p>
<button class="card__btn">Choose Plan</button>
</article>
<article class="card card--avatar">
<div class="avatar" aria-hidden="true"></div>
<header class="card__header">Circular Avatar</header>
<h3 class="card__title">Maya Kim</h3>
<p class="card__text">Let the avatar break the card edge to add hierarchy and presence.</p>
<button class="card__btn">View Profile</button>
</article>
<article class="card card--coupon">
<header class="card__header">Coupon Notch</header>
<h3 class="card__title">20% Off</h3>
<p class="card__text">Scalloped notches and perforation hint at savings and redemption.</p>
<button class="card__btn">Apply Code</button>
</article>
<article class="card card--tag">
<header class="card__header">Price Tag Tab</header>
<h3 class="card__title">Bundle</h3>
<p class="card__text">A trapezoid tab anchors pricing or labels without extra boxes.</p>
<button class="card__btn">See Details</button>
</article>
<article class="card card--starburst">
<header class="card__header">Starburst Sticker</header>
<h3 class="card__title">Flash Sale</h3>
<p class="card__text">A jagged sticker grabs the eye for time-sensitive promos.</p>
<button class="card__btn">Shop Now</button>
</article>
</section>
Step 2: The Basic CSS & Styling
Set up variables for colors, radii, and sizes. The grid uses auto-fit to wrap cards. Each card gets a consistent padding, radius, and subtle shadow so the shape accents can do the visual work. The modifier classes will overwrite or add layers with pseudo-elements.
/* CSS */
:root {
--bg: #0e1116;
--card: #171b22;
--text: #d7e1f3;
--muted: #8b98a7;
--accent: #6ee7ff;
--accent-2: #ff6ea8;
--success: #70ffa7;
--warning: #ffd166;
--radius: 14px;
--gap: 1.25rem;
--shadow: 0 6px 20px rgba(0,0,0,0.25);
}
*,
*::before,
*::after { box-sizing: border-box; }
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji";
background: radial-gradient(1200px 800px at 20% -10%, #18202a, var(--bg));
color: var(--text);
line-height: 1.45;
padding: 3rem 1rem;
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: var(--gap);
max-width: 1100px;
margin: 0 auto;
}
.card {
position: relative;
background: var(--card);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 1.1rem 1.1rem 1.25rem;
overflow: hidden;
isolation: isolate; /* keep pseudo-elements scoped */
}
.card__header {
font-size: 0.825rem;
color: var(--muted);
letter-spacing: 0.04em;
text-transform: uppercase;
margin-bottom: 0.4rem;
}
.card__title {
margin: 0.1rem 0 0.4rem;
font-size: 1.25rem;
}
.card__text {
margin: 0 0 0.9rem;
color: #c8d4e3;
}
.card__btn {
background: linear-gradient(180deg, #2a3340, #1c2530);
color: var(--text);
border: 1px solid #2a3a4b;
border-radius: 10px;
padding: 0.55rem 0.9rem;
cursor: pointer;
transition: transform .15s ease, box-shadow .15s ease;
}
.card__btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(0,0,0,0.25);
}
Advanced Tip: Use one set of custom properties for all cards. Then, per variant, adjust only the accent variables. This keeps design tokens centralized and makes dark/light theming a variable swap instead of a rewrite.
Step 3: Building the Shape Overlays (Ribbon, Avatar, Coupon)
These three treatments add decoration layers with pseudo-elements or a small child element. The goal is zero extra HTTP requests and fully themeable accents.
/* CSS */
/* 1) Corner ribbon using triangles */
.card--ribbon {
--ribbon: var(--accent);
--ribbon-fold: #57cadd;
}
.card--ribbon::before,
.card--ribbon::after {
content: "";
position: absolute;
z-index: 1;
top: 0;
right: 0;
width: 0; height: 0;
border-style: solid;
}
/* Main triangle */
.card--ribbon::before {
border-width: 0 0 80px 80px;
border-color: transparent transparent var(--ribbon) transparent;
}
/* Fold shadow triangle */
.card--ribbon::after {
border-width: 0 0 60px 60px;
border-color: transparent transparent var(--ribbon-fold) transparent;
transform: translate(-8px, 0);
filter: brightness(0.95);
}
/* 2) Circular avatar overlapping the edge */
.card--avatar { padding-top: 2.1rem; }
.card--avatar .avatar {
--size: 62px;
position: absolute;
z-index: 2;
top: -20px; left: 1rem;
width: var(--size);
height: var(--size);
border-radius: 50%;
background:
radial-gradient(circle at 30% 30%, #fff8, transparent 40%),
linear-gradient(160deg, var(--accent-2), #7e6bff);
box-shadow:
0 6px 20px rgba(0,0,0,0.35),
0 0 0 4px #0d131a; /* cutout ring to separate from card */
outline: 2px solid #0000; /* anchor for high contrast modes */
}
/* 3) Coupon notches and perforation */
.card--coupon {
--notch: 14px;
--perforation: #2a3441;
background:
radial-gradient(100% 60% at 0 0, #1a2029, var(--card)) no-repeat;
}
.card--coupon::before,
.card--coupon::after {
content: "";
position: absolute;
top: 54%;
transform: translateY(-50%);
width: calc(var(--notch) * 2);
height: calc(var(--notch) * 2);
border-radius: 50%;
background: var(--bg); /* matches page to fake a bite */
z-index: 1;
}
.card--coupon::before { left: calc(var(--notch) * -1); }
.card--coupon::after { right: calc(var(--notch) * -1); }
/* Perforation line */
.card--coupon .card__header {
position: relative;
padding-bottom: 0.65rem;
margin-bottom: 0.65rem;
}
.card--coupon .card__header::after {
content: "";
position: absolute;
left: -1.1rem; right: -1.1rem; bottom: 0;
height: 2px;
background:
repeating-linear-gradient(90deg, var(--perforation) 0 10px, transparent 10px 18px);
opacity: 0.9;
}
How This Works (Code Breakdown)
The ribbon uses two triangles drawn with borders. The main triangle sets border-width on the bottom and left edges and makes the other sides transparent, which yields a right-angled wedge. If you want a refresher or other triangle variants, see the guide on how to make a right-angled triangle with CSS. The second triangle is smaller and offset to suggest a folded ribbon tail. Both are absolutely positioned in the top-right corner so they sit above the card without covering content.
The circular avatar is a simple element with a fixed size and border-radius: 50%. It sits partly outside the card using a negative top offset. The ring effect comes from a box-shadow that matches the page background, which creates a clean separation from the card color. For other circular tricks, the library page on how to make a circle with CSS covers multiple patterns, including responsive sizing with percentages.
The coupon look needs two pieces. The side notches are full circles placed halfway down the card edges, but offset to the outside so they punch a semicircle out of the card, which reads like die-cut paper. The perforation uses a repeating-linear-gradient background on the header’s ::after to draw a dashed line across the card. This gives a visual cue for tear-off or a code section without extra elements.
Step 4: Building the Badge Stickers (Tag and Starburst)
These accents sit on the card corner and act like stickers or tabs. One uses a trapezoid with skew, the other uses a conic mask to generate a spiky burst.
/* CSS */
/* 4) Price-tag tab (trapezoid) */
.card--tag {
--tag-clr: var(--warning);
padding-top: 2rem;
}
.card--tag::before {
content: attr(data-tag);
position: absolute;
top: 0.6rem; left: -10px;
height: 34px; line-height: 34px;
padding: 0 14px 0 20px;
font-weight: 600;
color: #1a1f27;
background: var(--tag-clr);
transform: skewX(-18deg);
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
box-shadow: 0 6px 14px rgba(0,0,0,0.22);
}
/* Label text counter-skew */
.card--tag .card__header,
.card--tag .card__title {
position: relative;
}
.card--tag::before { content: "Deal"; }
.card--tag::after {
content: "";
position: absolute;
top: 1.4rem; left: 0;
width: 8px; height: 8px;
background: #1a1f27;
border-radius: 50%;
transform: translateX(4px);
box-shadow: -4px 0 0 0 var(--tag-clr); /* eyelet hole */
}
/* 5) Starburst sticker */
.card--starburst { padding-top: 2.2rem; }
.card--starburst::before {
content: "New";
position: absolute;
top: -20px; right: -20px;
width: 96px; height: 96px;
display: grid; place-items: center;
font-weight: 800;
color: #0e1116;
background: radial-gradient(circle at 35% 35%, #fff8, transparent 45%), #9bff9e;
filter: drop-shadow(0 8px 18px rgba(0,0,0,0.35));
/* Create spikes using a conic mask */
--spikes: 18;
--angle: calc(360deg / var(--spikes));
-webkit-mask:
conic-gradient(from 0deg,
#000 0 var(--angle), #0000 var(--angle) calc(var(--angle) * 1.5)) repeat;
mask:
conic-gradient(from 0deg,
#000 0 var(--angle), #0000 var(--angle) calc(var(--angle) * 1.5)) repeat;
-webkit-mask-size: 12deg 100%;
mask-size: 12deg 100%;
border-radius: 50%;
}
How This Works (Code Breakdown)
The price-tag tab fakes a trapezoid by skewing a rectangular badge to the left. The rounded right corners keep it friendly, and the small circular eyelet plus a box-shadow behind it suggests a punched hole. If you prefer a border-built trapezoid for stricter geometry, the approach is similar: a fixed-height element with transparent left and right borders and a solid bottom border will also read as a trapezoid when placed at an edge.
The starburst uses a conic-gradient mask to carve alternating transparent wedges from a circle. That mask repeats to create spikes, while the background supplies color and a subtle highlight. The content value on ::before provides a short label without extra markup. For more variations such as different spike counts or double rings, see the tutorial on how to make a starburst with CSS.
Advanced Techniques: Animations & Hover Effects
Shapes feel more polished with small motion. Keep motion light and purposeful. Aim for less than 150ms on hovers and use gentle transforms instead of box-shadow thrash.
/* CSS */
/* Micro-interactions */
.card--ribbon:hover::before { transform: translate(-1px, 1px) rotate(-1deg); }
.card--ribbon:hover::after { transform: translate(-8px, 1px) rotate(-1deg); }
.card--avatar .avatar { transition: transform .18s ease; }
.card--avatar:hover .avatar { transform: translateY(-2px) scale(1.03); }
.card--coupon .card__btn { transition: background .15s ease, transform .15s ease; }
.card--coupon:hover .card__btn { transform: translateY(-1px); background: linear-gradient(180deg, #334155, #243141); }
.card--tag::before { transition: transform .18s ease; }
.card--tag:hover::before { transform: skewX(-18deg) translateX(2px); }
@keyframes slow-rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(6deg); }
}
.card--starburst::before {
transition: transform .2s ease;
animation: slow-rotate 5s linear infinite alternate;
}
.card--starburst:hover::before { transform: scale(1.04); }
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
.card__btn,
.card--avatar .avatar,
.card--tag::before,
.card--starburst::before {
transition: none;
animation: none;
}
}
Accessibility & Performance
CSS shapes are decoration by default. The content still needs clear hierarchy, readable contrast, and sensible focus order. Avoid letting pseudo-elements cover focus rings or actionable text. Keep color choices within WCAG contrast ranges, especially if a shape sits behind a label.
Accessibility
Keep decorative shapes out of the accessibility tree. The pseudo-elements here are not announced by screen readers, which is what we want. The avatar element uses aria-hidden to mark it as decoration, since the user’s name appears as text in the title. If a sticker conveys meaning, reflect that meaning in visible text as well. For example, the starburst carries the word “New” in its content so the state is represented for all users. Pair motion with a reduced-motion fallback, as the code above does with @media (prefers-reduced-motion: reduce). Maintain focus styles on buttons and do not let absolute elements overlap focusable controls.
Performance
These effects are paint-only in most cases and fast on modern GPUs. Triangles made with borders are cheap. Border-radius and gradients are also solid. Try to animate transforms and opacity, not box-shadow or filters, since shadows trigger more costly repaints. Masks and conic-gradients are widely supported, but test the starburst on your target browsers and include a solid circular fallback if needed. Because everything is in CSS, there are no image assets to download and no extra layout from nested wrappers.
One Toolkit, Five Distinct Cards
You built five shape-driven card treatments: a corner ribbon, a circular avatar, coupon notches, a price-tag tab, and a starburst sticker. Each version uses variables, pseudo-elements, and a small amount of positioning, so you can reskin or scale them across a design system.
Use the same techniques to create other accents, like chevron footers or polygonal banners. If you want to expand your shape vocabulary, the library guides on how to make a right-angled triangle with CSS and how to make a circle with CSS pair well with the how to make a starburst with CSS article you used here. Now you have a practical pattern set to shape cards with intention, not just shadows.