One thumb-friendly primary action
A round button that floats above the content (bottom-right) for the screen's main action — compose, add, new. Big enough to tap easily.
Tap the blue + to open the menu.
HTML
<div class="phone">
<div class="fab-menu">
<button class="fab-action" data-action="photo">Photo</button>
<button class="fab-action" data-action="note">Note</button>
<button class="fab-action" data-action="link">Link</button>
</div>
<button class="fab" aria-label="New post">+</button>
</div>
CSS
.fab {
position: absolute;
bottom: 16px; right: 16px;
width: 52px; height: 52px; border-radius: 50%;
background: #38bdf8; color: #fff; border: none;
font-size: 1.5rem;
box-shadow: 0 8px 20px rgba(56,189,248,.45);
cursor: pointer;
transition: transform .2s ease;
z-index: 5;
}
.fab.open { transform: rotate(45deg); background: #0f172a; }
.fab-menu {
position: absolute;
bottom: 80px; right: 16px;
display: flex; flex-direction: column; gap: 8px;
opacity: 0; pointer-events: none;
transform: translateY(10px);
transition: opacity .2s ease, transform .2s ease;
z-index: 4;
}
.fab-menu.open { opacity: 1; pointer-events: auto; transform: translateY(0); }
.fab-action {
background: #ffffff; color: #1f2433;
border: 1px solid #e5e7eb;
border-radius: 999px;
padding: 8px 14px;
font-size: .85rem; font-weight: 600;
box-shadow: 0 4px 12px rgba(0,0,0,.08);
cursor: pointer;
}
.fab-action:hover { background: #f1f5f9; }
JavaScript
const fab = document.querySelector('.fab');
const menu = document.querySelector('.fab-menu');
fab.addEventListener('click', (e) => {
e.stopPropagation();
fab.classList.toggle('open');
menu.classList.toggle('open');
});
document.querySelectorAll('.fab-action').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const action = btn.dataset.action;
console.log('Run action:', action);
fab.classList.remove('open');
menu.classList.remove('open');
});
});
document.addEventListener('click', (e) => {
if (!fab.contains(e.target) && !menu.contains(e.target)) {
fab.classList.remove('open');
menu.classList.remove('open');
}
});
Flick through images
A horizontal row that snaps to each image as you swipe — pure CSS with scroll-snap. Drag sideways in the demo.
HTML
<div class="swipe-viewport">
<div class="swipe-track">
<div class="swipe-slide">1</div>
<div class="swipe-slide">2</div>
<div class="swipe-slide">3</div>
<div class="swipe-slide">4</div>
</div>
</div>
<div class="swipe-controls">
<button class="swipe-arrow" id="sg-prev">←</button>
<div class="swipe-dots">
<i class="on"></i><i></i><i></i><i></i>
</div>
<button class="swipe-arrow" id="sg-next">→</button>
</div>
CSS
.swipe-viewport {
overflow: hidden;
border-radius: 10px;
touch-action: pan-y;
}
.swipe-track {
display: flex;
transition: transform .35s ease;
will-change: transform;
cursor: grab;
}
.swipe-track.dragging { transition: none; cursor: grabbing; }
.swipe-slide {
flex: 0 0 100%;
height: 130px;
display: grid; place-items: center;
color: #fff; font-weight: 800; font-size: 1.2rem;
background: linear-gradient(135deg,#bae6fd,#9b7bff);
border-right: 2px solid #ffffff;
user-select: none;
}
.swipe-controls {
display: flex; align-items: center; justify-content: space-between;
margin-top: 12px;
}
.swipe-arrow {
background: #ffffff; border: 1px solid #e5e7eb;
width: 36px; height: 36px; border-radius: 50%;
font-size: 1rem; cursor: pointer; color: #1f2433;
}
.swipe-arrow:hover { background: #f1f5f9; }
.swipe-dots { display: flex; gap: 6px; }
.swipe-dots i {
width: 8px; height: 8px; border-radius: 50%;
background: #cbd5e1; transition: background .2s;
}
.swipe-dots i.on { background: #38bdf8; }
JavaScript
const viewport = document.querySelector('.swipe-viewport');
const track = document.querySelector('.swipe-track');
const slides = track.querySelectorAll('.swipe-slide');
const dots = document.querySelectorAll('.swipe-dots i');
const prevBtn = document.getElementById('sg-prev');
const nextBtn = document.getElementById('sg-next');
let index = 0;
let startX = 0;
let dragX = 0;
let dragging = false;
function goTo(i) {
index = Math.max(0, Math.min(slides.length - 1, i));
track.style.transform = `translateX(${-index * 100}%)`;
dots.forEach((d, n) => d.classList.toggle('on', n === index));
}
viewport.addEventListener('pointerdown', (e) => {
dragging = true;
startX = e.clientX;
dragX = 0;
track.classList.add('dragging');
viewport.setPointerCapture(e.pointerId);
});
viewport.addEventListener('pointermove', (e) => {
if (!dragging) return;
dragX = e.clientX - startX;
const pct = (dragX / viewport.clientWidth) * 100;
track.style.transform = `translateX(calc(${-index * 100}% + ${pct}%))`;
});
viewport.addEventListener('pointerup', () => {
if (!dragging) return;
dragging = false;
track.classList.remove('dragging');
const threshold = viewport.clientWidth * 0.2;
if (dragX < -threshold) goTo(index + 1);
else if (dragX > threshold) goTo(index - 1);
else goTo(index);
});
prevBtn.addEventListener('click', () => goTo(index - 1));
nextBtn.addEventListener('click', () => goTo(index + 1));
Stacked, swipeable cards
Cards piled with a slight offset — the “deck” look used for swipe-to-decide UIs. Scale and fade the cards behind to suggest depth.
Drag the top card left or right.
HTML
<div class="stack"> <div class="card c3">Next up</div> <div class="card c2">On deck</div> <div class="card c1">Top card — drag me</div> </div>
CSS
.stack {
position: relative;
height: 150px;
max-width: 280px;
margin: 0 auto;
}
.stack .card {
position: absolute;
left: 0; right: 0;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 16px;
box-shadow: 0 6px 16px rgba(0,0,0,.08);
color: #374151;
font-weight: 600;
user-select: none;
touch-action: none;
transition: transform .35s ease, opacity .35s ease;
}
.c1 { top: 0; z-index: 3; cursor: grab; }
.c1.dragging { transition: none; cursor: grabbing; }
.c2 { top: 14px; transform: scale(.96); opacity: .9; z-index: 2; }
.c3 { top: 28px; transform: scale(.92); opacity: .8; z-index: 1; }
.card.swipe-out-left { transform: translateX(-400px) rotate(-20deg); opacity: 0; }
.card.swipe-out-right { transform: translateX( 400px) rotate( 20deg); opacity: 0; }
JavaScript
const stack = document.getElementById('card-stack-demo');
const status = document.getElementById('stack-status');
function attachDrag(card) {
let startX = 0, dx = 0, dragging = false;
card.addEventListener('pointerdown', (e) => {
dragging = true;
startX = e.clientX;
dx = 0;
card.classList.add('dragging');
card.setPointerCapture(e.pointerId);
});
card.addEventListener('pointermove', (e) => {
if (!dragging) return;
dx = e.clientX - startX;
const rot = dx / 12;
card.style.transform = `translateX(${dx}px) rotate(${rot}deg)`;
});
card.addEventListener('pointerup', () => {
if (!dragging) return;
dragging = false;
card.classList.remove('dragging');
const threshold = 100;
if (dx > threshold) finishSwipe(card, 'right');
else if (dx < -threshold) finishSwipe(card, 'left');
else card.style.transform = '';
});
}
function finishSwipe(card, dir) {
card.style.transform = '';
card.classList.add(dir === 'left' ? 'swipe-out-left' : 'swipe-out-right');
status.textContent = `Swiped ${dir.toUpperCase()}: ${card.dataset.label}`;
setTimeout(() => {
card.remove();
promote();
}, 350);
}
function promote() {
const cards = stack.querySelectorAll('.card');
cards.forEach((c, i) => {
c.classList.remove('c1','c2','c3');
c.classList.add(['c1','c2','c3'][i] || 'c3');
});
const top = stack.querySelector('.c1');
if (top) attachDrag(top);
}
attachDrag(stack.querySelector('.c1'));
Drag-to-set range slider
A touch-driven range input: drag the handle along the track to set a value. Pointer events keep it working with both mouse and finger.
Drag the dot left or right to set a value between 0 and 100.
HTML
<div class="touch-slider">
<div class="touch-track" id="ts-track">
<div class="touch-fill" id="ts-fill"></div>
<div class="touch-thumb" id="ts-thumb"
role="slider" aria-valuemin="0" aria-valuemax="100"
aria-valuenow="40" tabindex="0"></div>
</div>
<div class="touch-readout">
<span>Volume</span>
<strong id="ts-value">40</strong>
</div>
</div>
CSS
.touch-slider { max-width: 360px; margin: 0 auto; }
.touch-track {
position: relative;
height: 8px;
background: #e5e7eb;
border-radius: 999px;
touch-action: none;
}
.touch-fill {
position: absolute; top: 0; left: 0; bottom: 0;
width: 40%;
background: #38bdf8;
border-radius: 999px;
}
.touch-thumb {
position: absolute;
top: 50%; left: 40%;
width: 22px; height: 22px;
margin-left: -11px; margin-top: -11px;
background: #ffffff;
border: 2px solid #38bdf8;
border-radius: 50%;
box-shadow: 0 4px 10px rgba(56,189,248,.45);
cursor: grab;
}
.touch-thumb.dragging { cursor: grabbing; background: #38bdf8; }
.touch-readout {
display: flex; justify-content: space-between;
margin-top: 14px;
font-size: .85rem; color: #374151;
}
.touch-readout strong { color: #38bdf8; font-size: 1rem; }
JavaScript
const track = document.getElementById('ts-track');
const fill = document.getElementById('ts-fill');
const thumb = document.getElementById('ts-thumb');
const readout = document.getElementById('ts-value');
let value = 40; // 0..100
let dragging = false;
function setValue(v) {
value = Math.max(0, Math.min(100, v));
fill.style.width = value + '%';
thumb.style.left = value + '%';
readout.textContent = Math.round(value);
thumb.setAttribute('aria-valuenow', Math.round(value));
}
function valueFromEvent(e) {
const rect = track.getBoundingClientRect();
const x = e.clientX - rect.left;
return (x / rect.width) * 100;
}
track.addEventListener('pointerdown', (e) => {
dragging = true;
thumb.classList.add('dragging');
track.setPointerCapture(e.pointerId);
setValue(valueFromEvent(e));
});
track.addEventListener('pointermove', (e) => {
if (!dragging) return;
setValue(valueFromEvent(e));
});
track.addEventListener('pointerup', () => {
dragging = false;
thumb.classList.remove('dragging');
});
setValue(value);
Keep building
See Mobile Navigation, Responsive Web Design, and Gallery Pages — or browse the full Component Library.