Category Drill-Down Carousel — HTML CSS JS Snippet
Category Drill-Down Carousel · Carousels · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="cdd-wrap">
<div class="cdd-tabs" id="cddTabs"></div>
<div class="cdd-viewport">
<div class="cdd-track" id="cddTrack"></div>
</div>
<div class="cdd-controls">
<button class="cdd-btn" id="cddPrev" aria-label="Previous">‹</button>
<div class="cdd-dots" id="cddDots"></div>
<button class="cdd-btn" id="cddNext" aria-label="Next">›</button>
</div>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f6f7f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.cdd-wrap{width:100%;max-width:460px}
.cdd-tabs{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}
.cdd-tab{font:600 12px system-ui,sans-serif;padding:7px 14px;border-radius:20px;border:1.5px solid #e3e5ea;background:#fff;color:#4b5563;cursor:pointer;transition:border-color .15s,color .15s,background .15s}
.cdd-tab:hover{border-color:#6366f1;color:#6366f1}
.cdd-tab-active{background:#6366f1;border-color:#6366f1;color:#fff}
.cdd-viewport{overflow:hidden;border-radius:14px}
.cdd-track{display:flex;transition:transform .4s cubic-bezier(.4,0,.2,1)}
.cdd-slide{flex:0 0 100%;height:170px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;color:#fff}
.cdd-slide span{font-size:40px}
.cdd-slide strong{font-size:15px;font-weight:800}
.cdd-controls{display:flex;align-items:center;justify-content:center;gap:16px;margin-top:16px}
.cdd-btn{width:36px;height:36px;border-radius:50%;background:#fff;border:1.5px solid #e3e5ea;color:#4b5563;font-size:17px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:border-color .15s,color .15s}
.cdd-btn:hover{border-color:#6366f1;color:#6366f1}
.cdd-dots{display:flex;gap:7px}
.cdd-dot{width:7px;height:7px;border-radius:50%;background:#d1d5db;border:none;cursor:pointer;transition:background .2s,width .2s}
.cdd-dot.active{background:#6366f1;width:20px;border-radius:4px}var DATA = {
audio: { label: 'Audio', items: [
{ icon: '🎧', title: 'Headphones', bg: '#6366f1,#4338ca' },
{ icon: '🔊', title: 'Speaker', bg: '#4f46e5,#312e81' },
] },
wearables: { label: 'Wearables', items: [
{ icon: '⌚', title: 'Smartwatch', bg: '#ec4899,#9d174d' },
{ icon: '🕶️', title: 'Smart Glasses', bg: '#db2777,#831843' },
] },
gaming: { label: 'Gaming', items: [
{ icon: '🎮', title: 'Controller', bg: '#10b981,#047857' },
{ icon: '🕹️', title: 'Console', bg: '#059669,#065f46' },
{ icon: '🎧', title: 'Gaming Headset', bg: '#14b8a6,#0f766e' },
] },
};
var tabsWrap = document.getElementById('cddTabs');
var track = document.getElementById('cddTrack');
var dotsWrap = document.getElementById('cddDots');
var activeCat = 'audio';
var current = 0;
Object.keys(DATA).forEach(function (key) {
var tab = document.createElement('button');
tab.className = 'cdd-tab' + (key === activeCat ? ' cdd-tab-active' : '');
tab.textContent = DATA[key].label;
tab.addEventListener('click', function () { switchCategory(key); });
tabsWrap.appendChild(tab);
});
var tabs = document.querySelectorAll('.cdd-tab');
function buildSlides() {
track.innerHTML = '';
dotsWrap.innerHTML = '';
DATA[activeCat].items.forEach(function (it, i) {
var slide = document.createElement('div');
slide.className = 'cdd-slide';
slide.style.background = 'linear-gradient(160deg,' + it.bg + ')';
slide.innerHTML = '<span>' + it.icon + '</span><strong>' + it.title + '</strong>';
track.appendChild(slide);
var dot = document.createElement('button');
dot.className = 'cdd-dot';
dot.setAttribute('aria-label', 'Go to item ' + (i + 1));
dot.addEventListener('click', function () { goTo(i); });
dotsWrap.appendChild(dot);
});
}
function render() {
track.style.transform = 'translateX(' + (-current * 100) + '%)';
document.querySelectorAll('.cdd-dot').forEach(function (d, i) { d.classList.toggle('active', i === current); });
}
function switchCategory(key) {
activeCat = key;
current = 0;
tabs.forEach(function (t) { t.classList.toggle('cdd-tab-active', t.textContent === DATA[key].label); });
buildSlides();
render();
}
function goTo(i) { current = i; render(); }
function next() { goTo((current + 1) % DATA[activeCat].items.length); }
function prev() { goTo((current - 1 + DATA[activeCat].items.length) % DATA[activeCat].items.length); }
document.getElementById('cddNext').addEventListener('click', next);
document.getElementById('cddPrev').addEventListener('click', prev);
document.addEventListener('keydown', function (e) {
if (e.key === 'ArrowRight') next();
else if (e.key === 'ArrowLeft') prev();
});
buildSlides();
render();Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Category Drill-Down Carousel — Tabs That Rebuild the Deck, Not Just Filter It

This is a two-level navigation structure disguised as one carousel: a row of category tabs sits above a slide deck, and clicking a tab doesn't filter the *existing* slides — it discards them and builds an entirely *new* deck from that category's own item list. Unlike the Filter-Reactive Carousel elsewhere in this library (which filters one flat pool of items down by tag), this one's categories don't share a pool at all — "Audio" has two items, "Gaming" has three, and the carousel's own length, dot count, and bounds all adjust to match whichever category is active.
Slides are rebuilt, dots are rebuilt, position always resets
switchCategory() does three things every time: sets activeCat, resets current back to 0, and calls buildSlides(), which clears and regenerates *both* the slide track and the dots from DATA[activeCat].items. That reset-to-zero is deliberate — carrying over an index from a 3-item category into a 2-item one could point at a slide that no longer exists, so switching categories always lands cleanly on that category's first item.
One data structure, two levels of navigation
DATA is a plain object keyed by category id, each holding a label (for the tab) and an items array (for the slides) — the tabs are generated from Object.keys(DATA), and the slide deck is generated from whichever category's items is currently active. Adding a whole new category, with its own independent item count, is one new key in that object — no other code changes.
Step by step
How to Use
- 1Paste HTML, CSS, and JSThree category tabs appear above a carousel showing "Audio"'s two items, starting on the first.
- 2Click "Gaming"The whole deck rebuilds — now three items, three dots, reset to the first one.
- 3Navigate within a categoryArrows, dots, and arrow keys move through that category's own items normally.
- 4Switch categories againPosition always resets to the first item of whichever category you land on.
- 5Add a fourth categoryAdd one new key to DATA with its own label and items array — a matching tab appears automatically.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Filter-Reactive Carousel filters ONE shared pool of items down by a tag — the underlying item set never changes, only which subset is shown. This snippet's categories are genuinely separate item lists with independent counts; switching categories discards the old deck and builds a completely different one.
Because each category can have a different number of items, carrying over an index from a longer category into a shorter one could point past the end of the new deck. Resetting to 0 guarantees the carousel always lands on a valid, existing slide.
Add one new key to the DATA object with its own label and its own items array (icons/titles/backgrounds) — a matching tab is generated automatically from Object.keys(DATA), with no other code changes required.
Yes — just include the same item object (or an equivalent duplicate) in both categories' items arrays; the data model treats each category's list independently, so overlap is entirely allowed.
Tabs, arrows, and dots are all real, labeled buttons, and the whole carousel is operable via Left/Right arrow keys; for full accessibility, add aria-selected to the active tab and consider role="tablist"/role="tab" semantics on the tab row.