This tutorial will guide you through creating interactive 3D product cards using HTML, CSS, and JavaScript. We’ll leverage the power of model-viewer for displaying 3D models and adding engaging hover effects. The target audience for this tutorial is beginners with basic HTML, CSS, and JavaScript knowledge.
Step 1: Setting up the Project
First, you need to include the model-viewer library in your project. This is done by adding a script tag within the <head> section of your HTML document.
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/4.0.0/model-viewer.min.js"></script>
Step 2: Creating the HTML Structure for 3D Product Cards
Next, create the HTML structure for your 3D product cards. Each card will contain a 3D model, a visually appealing glass effect, and some product information.
<div class="card">
<div class="overflow">
<div class="model">
<model-viewer src="https://assets.codepen.io/3421562/leaves_keyboard.glb" shadow-intensity="0.4"></model-viewer>
</div>
</div>
<div class="glass">
<div class="gradient-blur"><div></div><div></div><div></div><div></div><div></div><div></div></div>
</div>
<div class="content">
<h2>LeafKey</h2>
<p>A keyboard that brings the tranquility of the forest to your fingertips.</p>
<div class="options">
<div onclick="changeModelStyle(this, 0);" style="background: #f2c173;"></div>
<div onclick="changeModelStyle(this, 60);" style="background: #96dd78;"></div>
<div onclick="changeModelStyle(this, 110);" style="background: #6ee5bc;"></div>
</div>
</div>
</div>
<div class="card">
<div class="overflow">
<div class="model">
<model-viewer src="https://assets.codepen.io/3421562/topo_keyboard.glb" shadow-intensity="0.4"></model-viewer>
</div>
</div>
<div class="glass">
<div class="gradient-blur"><div></div><div></div><div></div><div></div><div></div><div></div></div>
</div>
<div class="content">
<h2>TopoKey</h2>
<p>Custom illuminated keyboard with neon topology mapped out in every key.</p>
<div class="options">
<div onclick="changeModelStyle(this, 0);" style="background: #2cb4f0;"></div>
<div onclick="changeModelStyle(this, 110);" style="background: #ff69e4;"></div>
<div onclick="changeModelStyle(this, 290);" style="background: #16b83f;"></div>
</div>
</div>
</div>
<div class="card">
<div class="overflow">
<div class="model">
<model-viewer src="https://assets.codepen.io/3421562/panda_keyboard.glb" shadow-intensity="0.4"></model-viewer>
</div>
</div>
<div class="glass">
<div class="gradient-blur"><div></div><div></div><div></div><div></div><div></div><div></div></div>
</div>
<div class="content">
<h2>PandaKey</h2>
<p>Panda, panda, panda, panda, panda, panda... panda</p>
<div class="options">
<div onclick="changeModelStyle(this, 0);" style="background: #1e1e1e;"></div>
<div onclick="changeModelStyle(this, 0, 1);" style="background: #eee;"></div>
</div>
</div>
</div>
Step 3: Styling with CSS
Now, let’s add some CSS to style the cards and create the 3D effect. This includes styling the card itself, the glass effect, the 3D model, and the product information.
@import url('https://fonts.cdnfonts.com/css/satoshi');
*{ box-sizing: border-box; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; scroll-behavior: smooth;}
html, body { height: 100%; }
body {
display: flex; flex-direction: row; flex-wrap: wrap; flex-wrap: nowrap;
justify-content: center; align-items: center; margin: 0; padding-top: 120px;
background: #ededed;
font-family: 'Satoshi', sans-serif;
--scale: clamp(200px, 22vw, 300px);
}
.card {
--clr: #7da072; --fclr: #f0ffe2;
&:nth-child(2) { --clr: #2e2e2e; --fclr: #fefefe; }
&:nth-child(3) { --clr: #fff; --fclr: #000; }
width: var(--scale); height: 364px;
min-width: var(--scale);
background: var(--clr);
border-radius: 8px;
box-shadow:
-8px -8px 24px 0 #fffc,
1px 1px 3px 0px #0002,
12px 42px 24px -8px #0001,
10px 24px 42px 0 #0001,
1px 4px 12px 0 #0002;
&:nth-child(1) {
box-shadow:
-8px -8px 24px 0 #fffc,
1px 1px 3px 0px #7da07277,
12px 42px 24px -8px #7da07233,
10px 24px 42px 0 #7da07233,
1px 4px 12px 0 #7da07244;
}
&:nth-child(2) {
box-shadow:
-8px -8px 24px 0 #fffc,
1px 1px 3px 0px #0006,
12px 42px 24px -8px #0003,
10px 24px 42px 0 #0003,
1px 4px 12px 0 #0006;
}
position: relative;
display: flex;
flex-direction: column;
justify-content: end; align-items: center;
cursor: pointer;
margin: auto 24px;
.content {
position: relative;
z-index: 1;
bottom: 0; top: unset;
height: fit-content;
padding: 12px 24px;
h2 , p {
margin: 0; padding: 0;
line-height: 124%;
margin-bottom: 8px;
font-weight: 700;
color: var(--fclr);
}
p {
font-weight: 500;
margin-bottom: 16px;
}
.options {
position: absolute;
display: flex; gap: 4px;
bottom: 8px; right: 12px;
> div {
cursor: pointer;
width: 24px; height: 16px;
border-radius: 4px;
background: white;
border: 1px solid #aaa8;
transition: border 0.1s ease-in-out;
&:hover {
border: 1px solid #fff;
}
}
}
}
.glass {
pointer-events: none;
position: absolute;
bottom: 0; left: 0;
width: 100%; height: 80%;
background: linear-gradient(transparent, var(--clr) 80%);
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
overflow: hidden;
}
.overflow {
pointer-events: none;
position: absolute;
width: 200%; height: 200%;
overflow: hidden;
display: flex; justify-content: end; align-items: end;
clip-path: polygon(0 0, 100% 0, 100% 60%, 75% 60%, 75% 100%, 25% 100%, 25% 60%, 0 60%);
}
.model {
position: absolute;
width: 100%; height: 100%;
bottom: -10%;
display: flex; justify-content: center; align-items: center;
model-viewer{
position: absolute;
width: 500px; height: 500px;
width: 500px; height: calc((5/3) * var(--scale));
--progress-bar-color: none;
--progress-bar-height: 0px;
opacity: 0;
transition: filter 0.4s ease-in-out;
&.loaded{
animation: onLoad 1s ease-in forwards;
}
}
}
@media (max-width: 700px) {
&:nth-child(3) {
display: none;
}
}
@media (max-width: 468px) {
&:nth-child(1) {
display: none;
}
}
}
@keyframes onLoad {
to { opacity: 1; }
}
.gradient-blur {
position: absolute;
z-index: 1;
height: 100%;
inset: auto 0 0 0;
pointer-events: none;
}
.gradient-blur:nth-child(2){
inset: auto 0;
}
.gradient-blur > div,
.gradient-blur::before,
.gradient-blur::after {
position: absolute;
inset: 0;
--p1: 0%;
--p2: 12.5%;
--p3: 25%;
--p4: 37.5%;
--p5: 50%;
--p6: 62.5%;
--p7: 75%;
--p8: 87.5%;
--p9: 100%;
}
.gradient-blur::before {
content: "";
z-index: 1;
backdrop-filter: blur(0.5px);
mask: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) var(--p1),
rgba(0, 0, 0, 1) var(--p2),
rgba(0, 0, 0, 1) var(--p3),
rgba(0, 0, 0, 0) var(--p4)
);
}
.gradient-blur > div:nth-of-type(1) {
z-index: 2;
backdrop-filter: blur(1px);
mask: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) var(--p2),
rgba(0, 0, 0, 1) var(--p3),
rgba(0, 0, 0, 1) var(--p4),
rgba(0, 0, 0, 0) var(--p5)
);
}
.gradient-blur > div:nth-of-type(2) {
z-index: 3;
backdrop-filter: blur(2px);
mask: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) var(--p3),
rgba(0, 0, 0, 1) var(--p4),
rgba(0, 0, 0, 1) var(--p5),
rgba(0, 0, 0, 0) var(--p6)
);
}
.gradient-blur > div:nth-of-type(3) {
z-index: 4;
backdrop-filter: blur(4px);
mask: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) var(--p4),
rgba(0, 0, 0, 1) var(--p5),
rgba(0, 0, 0, 1) var(--p6),
rgba(0, 0, 0, 0) var(--p7)
);
}
.gradient-blur > div:nth-of-type(4) {
z-index: 5;
backdrop-filter: blur(8px);
mask: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) var(--p5),
rgba(0, 0, 0, 1) var(--p6),
rgba(0, 0, 0, 1) var(--p7),
rgba(0, 0, 0, 0) var(--p8)
);
}
.gradient-blur > div:nth-of-type(5) {
z-index: 6;
backdrop-filter: blur(16px);
mask: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) var(--p6),
rgba(0, 0, 0, 1) var(--p7),
rgba(0, 0, 0, 1) var(--p8),
rgba(0, 0, 0, 0) var(--p9)
);
}
.gradient-blur > div:nth-of-type(6) {
z-index: 7;
backdrop-filter: blur(32px);
mask: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) var(--p7),
rgba(0, 0, 0, 1) var(--p8),
rgba(0, 0, 0, 1) var(--p9)
);
}
.gradient-blur::after {
content: "";
z-index: 8;
backdrop-filter: blur(64px);
mask: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) var(--p8),
rgba(0, 0, 0, 1) var(--p9)
);
}
Step 4: Adding Interactivity with JavaScript
Finally, we’ll add some JavaScript to make the cards interactive. This includes adding hover effects to the 3D models and color changing options.
(() => {
const modelViewers = document.querySelectorAll('model-viewer');
const cards = document.querySelectorAll('.card');
const defaultOrbit = '64deg 25deg 64m';
const hoverOrbit = '90deg -42deg 50m';
const defaultTarget = '8m 1m 1m';
const hoverTarget = '4m 1m 1m';
const applyOrbit = (modelViewer, orbit, target) => {
modelViewer.setAttribute('camera-orbit', orbit);
modelViewer.setAttribute('camera-target', target);
modelViewer.setAttribute('interpolation-decay', '124');
};
cards.forEach((card, index) => {
const modelViewer = modelViewers[index];
if (modelViewer) {
applyOrbit(modelViewer, defaultOrbit, defaultTarget);
card.addEventListener('mouseenter', () => applyOrbit(modelViewer, hoverOrbit, hoverTarget));
card.addEventListener('mouseleave', () => applyOrbit(modelViewer, defaultOrbit, defaultTarget));
modelViewer.addEventListener('load', () => {
modelViewer.classList.add('loaded');
});
} else {
console.log(`No model found for card at i:${index}`);
}
});
})();
function changeModelStyle(element, deg, invert = 0) {
const card = element.closest('.card');
const modelViewer = card.querySelector('model-viewer');
if (modelViewer) { modelViewer.style.filter = `hue-rotate(${deg}deg) invert(${invert})`; }
}
Congratulations! You’ve successfully created interactive 3D product cards using HTML, CSS and JavaScript. Remember to replace placeholder image and model URLs with your own assets.



