← Component Library
Mobile Components

Mobile Patterns

Touch-friendly UI for small screens — floating buttons, swipe galleries, card stacks, and sliders. Each with a live demo and the HTML & CSS.

01 · Floating Action Button

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.

Your feed…

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');
  }
});
Keep it to one FAB per screen, and make the tap target at least 44×44px for comfortable thumbs.
03 · Card Stack

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.

Next up
On deck
Top card — drag me

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'));
The offset + scale is pure CSS; the actual swipe-to-dismiss gesture needs a little JavaScript (pointer events).
04 · Touch Slider

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.

Volume 40

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);
05 · Related

Keep building

See Mobile Navigation, Responsive Web Design, and Gallery Pages — or browse the full Component Library.