Badges, awards, and celebratory graphics often need more than plain text. With modern web technologies, you can create 3D interactive effects that look polished, respond to user actions, and bring personality to your site. In this post, we’ll build a 3D animated coin-like badge using HTML, CSS, SVG, and JavaScript.
🎯 What We’re Building
- A circular badge with a golden edge.
- Curved text and stars around the border.
- A raised center with animated numbers and labels.
- Particle sparkles floating inside.
- 3D tilt on mouse move.
- Ripple effect on click.
- Smooth animations and gradients.
Here’s the breakdown of how to achieve it step by step.
🏗️ Step 1: HTML Structure
We’ll start with a container and build layers inside it.
<div class="badge-container">
<div class="coin-edge"></div>
<svg class="badge-svg" viewBox="0 0 200 200">
<!-- Outer border and gradient -->
<circle cx="100" cy="100" r="95" class="outer-circle" />
<circle cx="100" cy="100" r="85" class="inner-circle" />
<!-- Text paths for curved text -->
<defs>
<path id="circle-top" d="M20,100a80,80 0 1,1 160,0a80,80 0 1,1 -160,0" />
<path id="circle-bottom" d="M180,100a80,80 0 1,1 -160,0a80,80 0 1,1 160,0" />
</defs>
<text class="curved-text">
<textPath href="#circle-top" startOffset="50%" text-anchor="middle">
20 YEARS ORLANDO & SURROUNDING CITIES
</textPath>
</text>
<text class="curved-stars">
<textPath href="#circle-bottom" startOffset="50%" text-anchor="middle">
★★★★★
</textPath>
</text>
</svg>
<div class="inner-content-wrapper">
<div class="content2">
<span class="click-effect"></span>
<div class="twenty">20</div>
<div class="years">YEARS</div>
<div class="location">ORLANDO & SURROUNDING CITIES</div>
</div>
</div>
<div class="particles"></div>
</div>
This gives us:
-
.coin-edge
: background rim. -
<svg>
: for border circles + curved text. -
.inner-content-wrapper
: raised middle with text. -
.particles
: container for sparkle elements.
✨ Step 2: How the Curved Text Works
One of the key visual features is the curved text around the badge. This is accomplished using SVG <textPath>
elements that attach text to a circular <path>
.
1. Define the Paths
Inside <defs>
, we define invisible circular paths:
<defs>
<path id="circle-top" d="M20,100a80,80 0 1,1 160,0a80,80 0 1,1 -160,0" />
<path id="circle-bottom" d="M180,100a80,80 0 1,1 -160,0a80,80 0 1,1 160,0" />
</defs>
These paths describe two arcs:
-
circle-top
: curves text along the top half. -
circle-bottom
: curves text along the bottom half.
2. Attach Text with <textPath>
<text class="curved-text">
<textPath href="#circle-top" startOffset="50%" text-anchor="middle">
20 YEARS ORLANDO & SURROUNDING CITIES
</textPath>
</text>
-
href="#circle-top"
: attaches the text to the top path. -
startOffset="50%"
: centers the text horizontally. -
text-anchor="middle"
: ensures text alignment is balanced.
Similarly, the stars at the bottom use the circle-bottom
path.
3. Adjusting Length with JavaScript (Optional)
If you want the text to fit perfectly along the arc, you can use JavaScript to measure the path length and set textLength
dynamically:
function fitTextToArc(textElement, pathId) {
const path = document.querySelector(pathId);
const pathLength = path.getTotalLength();
textElement.setAttribute('textLength', pathLength);
textElement.setAttribute('lengthAdjust', 'spacing');
}
fitTextToArc(document.querySelector('.curved-text textPath'), '#circle-top');
fitTextToArc(document.querySelector('.curved-stars textPath'), '#circle-bottom');
This ensures the text stretches evenly across the arc.
🎨 Step 3: CSS Styling & Animations
Core Layout
.badge-container {
position: relative;
width: 300px;
height: 300px;
perspective: 1000px;
animation: badgePulse 4s infinite;
}
.coin-edge {
position: absolute;
inset: 0;
border-radius: 50%;
background: radial-gradient(circle, #ffcc33, #b8860b);
box-shadow: inset 0 0 20px rgba(0,0,0,0.4);
}
.badge-svg {
position: relative;
width: 100%;
height: 100%;
z-index: 2;
}
.outer-circle {
fill: none;
stroke: #ffd700;
stroke-width: 6;
}
.inner-circle {
fill: url(#badgeGradient);
}
Center Content
.inner-content-wrapper {
position: absolute;
top: 50%;
left: 50%;
width: 60%;
height: 60%;
border-radius: 50%;
background: radial-gradient(circle, #fff, #f5f5f5);
transform: translate(-50%, -50%) translateZ(40px);
box-shadow: 0 8px 15px rgba(0,0,0,0.4);
}
.content2 {
text-align: center;
position: relative;
padding: 20px;
}
.twenty {
font-size: 3rem;
font-weight: bold;
color: #d4af37;
}
.years {
font-size: 1.5rem;
margin-top: -10px;
color: #333;
}
.location {
font-size: 0.9rem;
color: #666;
}
Animations
@keyframes badgePulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
}
.particle {
position: absolute;
width: 5px;
height: 5px;
background: white;
border-radius: 50%;
opacity: 0;
animation: sparkle 5s infinite;
}
@keyframes sparkle {
0% { transform: translateY(0); opacity: 0; }
50% { opacity: 1; }
100% { transform: translateY(-50px); opacity: 0; }
}
.click-effect {
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255,215,0,0.5);
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}
.click-effect.active {
animation: ripple 0.6s ease-out;
}
@keyframes ripple {
from { width: 0; height: 0; opacity: 0.8; }
to { width: 200px; height: 200px; opacity: 0; }
}
⚙️ Step 4: JavaScript Interactivity
We’ll add:
- count-up animation,
- particle generation,
- tilt effect,
- click ripple.
document.addEventListener('DOMContentLoaded', () => {
const badge = document.querySelector('.badge-container');
const badge3d = document.createElement('div');
badge3d.classList.add('badge-3d');
while (badge.firstChild) badge3d.appendChild(badge.firstChild);
badge.appendChild(badge3d);
// Particle creation
const particles = badge.querySelector('.particles');
for (let i = 0; i < 20; i++) {
const p = document.createElement('div');
p.classList.add('particle');
p.style.left = `${Math.random() * 100}%`;
p.style.top = `${Math.random() * 100}%`;
p.style.animationDelay = `${Math.random() * 5}s`;
particles.appendChild(p);
}
// Count-up
const num = document.querySelector('.twenty');
let count = 1;
const target = 20;
const interval = setInterval(() => {
num.textContent = count;
if (count >= target) clearInterval(interval);
count++;
}, 75);
// Tilt effect
badge.addEventListener('mousemove', (e) => {
const rect = badge.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width - 0.5;
const y = (e.clientY - rect.top) / rect.height - 0.5;
badge3d.style.transform = `rotateX(${y * 20}deg) rotateY(${x * -20}deg)`;
});
badge.addEventListener('mouseleave', () => {
badge3d.style.transform = 'rotateX(0) rotateY(0)';
});
// Ripple on click
badge.addEventListener('click', () => {
const ripple = document.querySelector('.click-effect');
ripple.classList.remove('active');
void ripple.offsetWidth; // reflow
ripple.classList.add('active');
});
});
✅ Wrap-Up
With just HTML, CSS, and vanilla JS, we created a polished, interactive badge that:
- tilts in 3D,
- sparkles with particles,
- animates numbers,
- pulses with life,
- features curved text along arcs,
- and reacts to clicks.
This same technique can be adapted for:
- Achievement badges,
- Loyalty program tokens,
- Event anniversary logos,
- Gamification UI elements.
Try it out, customize the colors and text, and make it your own! 🚀
Top comments (0)