UI Components Medium
Swipe Tabs
Horizontally swipeable tab panels with a sliding indicator. Supports touch swipe gestures and tap navigation. No libraries.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: #f5f5f7;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
}
.app {
width: 100%;
max-width: 480px;
min-height: 100vh;
background: #fff;
box-shadow: 0 0 40px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
overflow: hidden;
}
.app-header {
padding: 24px 20px 0;
}
.app-header h1 {
font-size: 22px;
font-weight: 700;
color: #111;
}
.app-header p {
font-size: 13px;
color: #888;
margin-top: 4px;
}
/* Tab bar */
.tabs-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
margin-top: 12px;
}
.tab-bar {
display: flex;
position: relative;
border-bottom: 1px solid #e8e8e8;
flex-shrink: 0;
}
.tab {
flex: 1;
padding: 12px 4px;
border: none;
background: none;
font-size: 13px;
font-weight: 600;
color: #999;
cursor: pointer;
transition: color 0.2s;
position: relative;
z-index: 1;
}
.tab.active {
color: #6366f1;
}
.tab-indicator {
position: absolute;
bottom: -1px;
height: 2px;
background: #6366f1;
border-radius: 1px;
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1), width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
/* Panels */
.panels-track {
display: flex;
flex: 1;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
touch-action: pan-y;
will-change: transform;
min-height: 400px;
}
.panels-track.dragging {
transition: none;
}
.panel {
min-width: 100%;
padding: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
/* Cards */
.card {
display: flex;
gap: 12px;
padding: 14px;
background: #f9fafb;
border-radius: 12px;
align-items: flex-start;
}
.card strong {
font-size: 14px;
color: #111;
display: block;
margin-bottom: 4px;
}
.card p {
font-size: 13px;
color: #555;
line-height: 1.5;
}
.avatar {
width: 38px;
height: 38px;
border-radius: 50%;
flex-shrink: 0;
}
.av1 {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
}
.av2 {
background: linear-gradient(135deg, #f59e0b, #ef4444);
}
.av3 {
background: linear-gradient(135deg, #10b981, #3b82f6);
}
/* Trending */
.trend-item {
display: flex;
align-items: center;
gap: 16px;
padding: 14px;
background: #f9fafb;
border-radius: 12px;
}
.rank {
font-size: 22px;
font-weight: 800;
color: #e5e7eb;
min-width: 28px;
}
.trend-item strong {
font-size: 15px;
color: #111;
display: block;
}
.trend-item p {
font-size: 12px;
color: #888;
margin-top: 2px;
}
/* Following */
.follow-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
background: #f9fafb;
border-radius: 12px;
}
.follow-info {
flex: 1;
}
.follow-info strong {
font-size: 14px;
color: #111;
display: block;
}
.follow-info p {
font-size: 12px;
color: #888;
margin-top: 2px;
}
.follow-btn {
padding: 7px 14px;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
border: 1.5px solid #6366f1;
color: #6366f1;
background: none;
transition: all 0.15s;
white-space: nowrap;
}
.follow-btn.active {
background: #6366f1;
color: #fff;
}
/* Saved */
.saved-item {
display: flex;
gap: 12px;
align-items: center;
padding: 12px;
background: #f9fafb;
border-radius: 12px;
}
.saved-thumb {
width: 52px;
height: 52px;
border-radius: 10px;
flex-shrink: 0;
}
.th1 {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
}
.th2 {
background: linear-gradient(135deg, #f59e0b, #ef4444);
}
.th3 {
background: linear-gradient(135deg, #10b981, #06b6d4);
}
.saved-item strong {
font-size: 14px;
color: #111;
display: block;
}
.saved-item p {
font-size: 12px;
color: #888;
margin-top: 4px;
}const tabs = document.querySelectorAll(".tab");
const track = document.getElementById("panelsTrack");
const indicator = document.querySelector(".tab-indicator");
let currentIndex = 0;
let startX = 0;
let deltaX = 0;
let dragging = false;
function updateIndicator(index) {
const tab = tabs[index];
indicator.style.left = `${tab.offsetLeft}px`;
indicator.style.width = `${tab.offsetWidth}px`;
}
function goTo(index) {
if (index < 0 || index >= tabs.length) return;
currentIndex = index;
track.style.transform = `translateX(-${index * 100}%)`;
tabs.forEach((t, i) => {
t.classList.toggle("active", i === index);
t.setAttribute("aria-selected", String(i === index));
});
updateIndicator(index);
}
tabs.forEach((tab) => {
tab.addEventListener("click", () => goTo(Number(tab.dataset.index)));
});
// Touch swipe support
track.addEventListener(
"touchstart",
(e) => {
startX = e.touches[0].clientX;
dragging = true;
track.classList.add("dragging");
},
{ passive: true }
);
track.addEventListener(
"touchmove",
(e) => {
if (!dragging) return;
deltaX = e.touches[0].clientX - startX;
const offset = -(currentIndex * 100) + (deltaX / track.offsetWidth) * 100;
track.style.transform = `translateX(${offset}%)`;
},
{ passive: true }
);
track.addEventListener("touchend", () => {
dragging = false;
track.classList.remove("dragging");
const threshold = track.offsetWidth * 0.25;
if (deltaX < -threshold) goTo(currentIndex + 1);
else if (deltaX > threshold) goTo(currentIndex - 1);
else goTo(currentIndex);
deltaX = 0;
});
// Init
updateIndicator(0);
window.addEventListener("resize", () => updateIndicator(currentIndex));<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" />
<title>Swipe Tabs</title>
</head>
<body>
<div class="app">
<header class="app-header">
<h1>Swipe Tabs</h1>
<p>Swipe left/right or tap tabs to navigate</p>
</header>
<div class="tabs-container">
<div class="tab-bar" role="tablist">
<button class="tab active" data-index="0" role="tab" aria-selected="true">Feed</button>
<button class="tab" data-index="1" role="tab" aria-selected="false">Trending</button>
<button class="tab" data-index="2" role="tab" aria-selected="false">Following</button>
<button class="tab" data-index="3" role="tab" aria-selected="false">Saved</button>
<div class="tab-indicator" aria-hidden="true"></div>
</div>
<div class="panels-track" id="panelsTrack">
<div class="panel" role="tabpanel">
<div class="card">
<div class="avatar av1"></div>
<div><strong>Alex Morgan</strong><p>Just shipped a new mobile navigation pattern — swipe tabs feel really natural on touch devices.</p></div>
</div>
<div class="card">
<div class="avatar av2"></div>
<div><strong>Sam Rivera</strong><p>The new iOS gestures are a game-changer for UX design. Momentum scrolling everywhere.</p></div>
</div>
<div class="card">
<div class="avatar av3"></div>
<div><strong>Jordan Lee</strong><p>Looking for feedback on this swipe-to-dismiss interaction in the mail app.</p></div>
</div>
</div>
<div class="panel" role="tabpanel">
<div class="trend-item"><span class="rank">1</span><div><strong>#MobileFirst</strong><p>12.4K posts today</p></div></div>
<div class="trend-item"><span class="rank">2</span><div><strong>#TouchUI</strong><p>8.1K posts today</p></div></div>
<div class="trend-item"><span class="rank">3</span><div><strong>#SwipeGestures</strong><p>5.3K posts today</p></div></div>
<div class="trend-item"><span class="rank">4</span><div><strong>#PWA</strong><p>4.7K posts today</p></div></div>
</div>
<div class="panel" role="tabpanel">
<div class="follow-item">
<div class="avatar av1"></div>
<div class="follow-info"><strong>Alex Morgan</strong><p>Senior UX Designer</p></div>
<button class="follow-btn active">Following</button>
</div>
<div class="follow-item">
<div class="avatar av2"></div>
<div class="follow-info"><strong>Sam Rivera</strong><p>iOS Developer</p></div>
<button class="follow-btn active">Following</button>
</div>
<div class="follow-item">
<div class="avatar av3"></div>
<div class="follow-info"><strong>Jordan Lee</strong><p>Product Manager</p></div>
<button class="follow-btn">Follow</button>
</div>
</div>
<div class="panel" role="tabpanel">
<div class="saved-item">
<div class="saved-thumb th1"></div>
<div><strong>Bottom Sheet Patterns</strong><p>Saved 2 days ago</p></div>
</div>
<div class="saved-item">
<div class="saved-thumb th2"></div>
<div><strong>Touch Gesture Guide</strong><p>Saved 1 week ago</p></div>
</div>
<div class="saved-item">
<div class="saved-thumb th3"></div>
<div><strong>Mobile-First CSS</strong><p>Saved 2 weeks ago</p></div>
</div>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Swipe Tabs
Tab navigation where panels can be swiped horizontally on touch devices. The tab indicator slides to follow the active tab, and swipe velocity determines whether the panel snaps to the next tab or springs back.
How it works
- Tab buttons update the active tab and animate the sliding underline indicator
touchstart/touchmove/touchendtrack horizontal swipe delta- Panels are laid out horizontally in a
flextrack;translateXshifts the visible panel - If swipe delta exceeds 25% of container width, the tab advances or retreats
Performance
- Uses
will-change: transformon the track for GPU compositing touch-action: pan-yallows native vertical scroll to work alongside horizontal swipe
When to use it
- Mobile feeds with multiple content categories (Feed, Trending, Following)
- Settings pages with grouped sections
- Any layout that needs a lightweight, swipeable tab bar