16 CSS Mobile Navigation Patterns 16 / 16

Neumorphic Bottom Navigation

A soft-UI neumorphic bottom navigation bar on a warm light-gray background.

Pure CSS MIT licensed
Live Demo Open in tab

This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.

Open in playground

The code

<div class="mn-16">
  <input type="radio" name="mn-16-tab" id="mn-16-t1" checked>
  <input type="radio" name="mn-16-tab" id="mn-16-t2">
  <input type="radio" name="mn-16-tab" id="mn-16-t3">
  <input type="radio" name="mn-16-tab" id="mn-16-t4">

  <div class="mn-16__pages">
    <div class="mn-16__page" data-p="1">
      <div class="mn-16__page-title">Overview</div>
      <div class="mn-16__page-sub">Good morning, Taylor ☀️</div>
      <div class="mn-16__stat-row" style="margin-bottom:14px">
        <div class="mn-16__stat-pill"><div class="val">7.4k</div><div class="lbl">Steps</div></div>
        <div class="mn-16__stat-pill"><div class="val">420</div><div class="lbl">Calories</div></div>
        <div class="mn-16__stat-pill"><div class="val">6.5h</div><div class="lbl">Sleep</div></div>
      </div>
      <div class="mn-16__neu-card">
        <h4>Daily Goal</h4>
        <p>74% complete — keep going!</p>
      </div>
      <div class="mn-16__neu-inset">
        <h4>Steps Progress</h4>
        <div class="mn-16__progress-track"><div class="mn-16__progress-fill" style="width:74%"></div></div>
        <div class="mn-16__progress-label"><span>7,400 steps</span><span>10,000</span></div>
      </div>
    </div>

    <div class="mn-16__page" data-p="2">
      <div class="mn-16__page-title">Workouts</div>
      <div class="mn-16__page-sub">This week's activity</div>
      <div class="mn-16__icon-row">
        <div class="mn-16__neu-icon-btn">🏃</div>
        <div class="mn-16__neu-icon-btn">🚴</div>
        <div class="mn-16__neu-icon-btn">🏊</div>
        <div class="mn-16__neu-icon-btn">🧘</div>
      </div>
      <div class="mn-16__neu-card">
        <h4>Morning Run</h4>
        <p>5.2 km · 28 min · 310 cal burned</p>
      </div>
      <div class="mn-16__neu-card">
        <h4>Strength Training</h4>
        <p>45 min · Full body · 280 cal burned</p>
      </div>
    </div>

    <div class="mn-16__page" data-p="3">
      <div class="mn-16__page-title">Nutrition</div>
      <div class="mn-16__page-sub">Today's intake</div>
      <div class="mn-16__neu-inset">
        <h4>Calories</h4>
        <div class="mn-16__progress-track"><div class="mn-16__progress-fill" style="width:60%;background:linear-gradient(90deg,#43d9b0,#4f7cff)"></div></div>
        <div class="mn-16__progress-label"><span>1,240 eaten</span><span>2,000 goal</span></div>
      </div>
      <div class="mn-16__neu-card">
        <h4>Protein · 68g</h4>
        <p>Chicken, eggs, Greek yogurt</p>
      </div>
      <div class="mn-16__neu-card">
        <h4>Carbs · 142g</h4>
        <p>Oats, rice, banana</p>
      </div>
    </div>

    <div class="mn-16__page" data-p="4">
      <div class="mn-16__page-title">Profile</div>
      <div class="mn-16__page-sub">Your health data</div>
      <div class="mn-16__neu-card" style="display:flex;align-items:center;gap:16px">
        <div style="width:52px;height:52px;border-radius:50%;background:linear-gradient(135deg,#4f7cff,#43d9b0);display:flex;align-items:center;justify-content:center;font-size:24px;box-shadow:3px 3px 8px var(--shadow-dark),-3px -3px 8px var(--shadow-light)">🏃</div>
        <div><h4>Taylor Kim</h4><p style="font-size:12px;color:var(--muted)">Premium · Since 2022</p></div>
      </div>
      <div class="mn-16__neu-inset">
        <h4>Weekly Streak</h4>
        <div style="display:flex;gap:8px;margin-top:6px">
          <div style="flex:1;height:32px;border-radius:8px;background:linear-gradient(135deg,#4f7cff,#43d9b0);opacity:1"></div>
          <div style="flex:1;height:32px;border-radius:8px;background:linear-gradient(135deg,#4f7cff,#43d9b0);opacity:1"></div>
          <div style="flex:1;height:32px;border-radius:8px;background:linear-gradient(135deg,#4f7cff,#43d9b0);opacity:1"></div>
          <div style="flex:1;height:32px;border-radius:8px;background:linear-gradient(135deg,#4f7cff,#43d9b0);opacity:0.4"></div>
          <div style="flex:1;height:32px;border-radius:8px;background:linear-gradient(135deg,#4f7cff,#43d9b0);opacity:1"></div>
          <div style="flex:1;height:32px;border-radius:8px;background:var(--bg);box-shadow:inset 2px 2px 5px var(--shadow-dark),inset -2px -2px 5px var(--shadow-light)"></div>
          <div style="flex:1;height:32px;border-radius:8px;background:var(--bg);box-shadow:inset 2px 2px 5px var(--shadow-dark),inset -2px -2px 5px var(--shadow-light)"></div>
        </div>
      </div>
    </div>
  </div>

  <div class="mn-16__navbar">
    <label for="mn-16-t1" class="mn-16__nav-item">
      <div class="mn-16__nav-icon">🏠</div>
      <span class="mn-16__nav-label">Home</span>
    </label>
    <label for="mn-16-t2" class="mn-16__nav-item">
      <div class="mn-16__nav-icon">🏃</div>
      <span class="mn-16__nav-label">Workout</span>
    </label>
    <label for="mn-16-t3" class="mn-16__nav-item">
      <div class="mn-16__nav-icon">🥗</div>
      <span class="mn-16__nav-label">Nutrition</span>
    </label>
    <label for="mn-16-t4" class="mn-16__nav-item">
      <div class="mn-16__nav-icon">👤</div>
      <span class="mn-16__nav-label">Profile</span>
    </label>
  </div>
</div>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body { display: flex; align-items: center; justify-content: center; min-height: 100vh; background: #0f0f13; font-family: 'Segoe UI', sans-serif; }

.mn-16 {
  --bg: #e0e5ec;
  --text: #2d3748;
  --muted: #8899aa;
  --accent: #4f7cff;
  --accent2: #ff6b9d;
  --accent3: #43d9b0;
  --accent4: #ffb347;
  --shadow-light: rgba(255,255,255,0.8);
  --shadow-dark: rgba(163,177,198,0.7);
  width: 375px;
  height: 667px;
  position: relative;
  overflow: hidden;
  background: var(--bg);
  border-radius: 32px;
  box-shadow: 0 30px 80px rgba(0,0,0,0.5);
}

.mn-16 input[type="radio"] { display: none; }

/* Page area */
.mn-16__pages {
  position: absolute;
  top: 0; left: 0; right: 0;
  bottom: 88px;
  padding: 32px 24px 16px;
}

.mn-16__page {
  position: absolute;
  inset: 0;
  padding: 32px 24px 16px;
  opacity: 0;
  pointer-events: none;
  transform: translateY(12px);
  transition: opacity 0.3s, transform 0.3s cubic-bezier(0.4,0,0.2,1);
}

#mn-16-t1:checked ~ .mn-16__pages .mn-16__page[data-p="1"],
#mn-16-t2:checked ~ .mn-16__pages .mn-16__page[data-p="2"],
#mn-16-t3:checked ~ .mn-16__pages .mn-16__page[data-p="3"],
#mn-16-t4:checked ~ .mn-16__pages .mn-16__page[data-p="4"] {
  opacity: 1;
  pointer-events: all;
  transform: translateY(0);
}

/* Page heading */
.mn-16__page-title {
  font-size: 26px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.5px;
  margin-bottom: 4px;
}
.mn-16__page-sub { font-size: 13px; color: var(--muted); margin-bottom: 28px; }

/* Neumorphic cards */
.mn-16__neu-card {
  background: var(--bg);
  border-radius: 20px;
  padding: 20px;
  margin-bottom: 14px;
  box-shadow:
    6px 6px 14px var(--shadow-dark),
    -6px -6px 14px var(--shadow-light);
}
.mn-16__neu-card h4 { font-size: 14px; font-weight: 600; color: var(--text); margin-bottom: 6px; }
.mn-16__neu-card p { font-size: 12px; color: var(--muted); line-height: 1.6; }

/* Neumorphic inset card */
.mn-16__neu-inset {
  background: var(--bg);
  border-radius: 16px;
  padding: 16px;
  margin-bottom: 14px;
  box-shadow:
    inset 4px 4px 10px var(--shadow-dark),
    inset -4px -4px 10px var(--shadow-light);
}
.mn-16__neu-inset h4 { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 8px; }

/* Progress bar */
.mn-16__progress-track {
  height: 8px;
  border-radius: 4px;
  background: var(--bg);
  box-shadow: inset 2px 2px 5px var(--shadow-dark), inset -2px -2px 5px var(--shadow-light);
  overflow: hidden;
  margin-bottom: 6px;
}
.mn-16__progress-fill {
  height: 100%;
  border-radius: 4px;
  background: linear-gradient(90deg, var(--accent), var(--accent3));
}
.mn-16__progress-label {
  display: flex; justify-content: space-between;
  font-size: 11px; color: var(--muted);
}

/* Icon row */
.mn-16__icon-row {
  display: flex; gap: 12px; margin-bottom: 14px;
}
.mn-16__neu-icon-btn {
  width: 52px; height: 52px;
  border-radius: 14px;
  background: var(--bg);
  display: flex; align-items: center; justify-content: center;
  font-size: 22px;
  box-shadow: 4px 4px 10px var(--shadow-dark), -4px -4px 10px var(--shadow-light);
  cursor: pointer;
  transition: box-shadow 0.15s;
}
.mn-16__neu-icon-btn:active {
  box-shadow: inset 3px 3px 8px var(--shadow-dark), inset -3px -3px 8px var(--shadow-light);
}

/* Bottom nav bar */
.mn-16__navbar {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  height: 88px;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: space-around;
  padding: 0 16px 12px;
  box-shadow: 0 -2px 20px rgba(163,177,198,0.4);
}

.mn-16__nav-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  cursor: pointer;
  padding: 8px 16px;
  border-radius: 16px;
  transition: all 0.2s;
  position: relative;
}

/* Inset pressed state for active tab */
#mn-16-t1:checked ~ .mn-16__navbar label[for="mn-16-t1"],
#mn-16-t2:checked ~ .mn-16__navbar label[for="mn-16-t2"],
#mn-16-t3:checked ~ .mn-16__navbar label[for="mn-16-t3"],
#mn-16-t4:checked ~ .mn-16__navbar label[for="mn-16-t4"] {
  box-shadow: inset 3px 3px 8px var(--shadow-dark), inset -3px -3px 8px var(--shadow-light);
}

.mn-16__nav-icon {
  font-size: 22px;
  line-height: 1;
  transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.mn-16__nav-label {
  font-size: 10px;
  font-weight: 600;
  color: var(--muted);
  transition: color 0.2s;
}

#mn-16-t1:checked ~ .mn-16__navbar label[for="mn-16-t1"] .mn-16__nav-label { color: var(--accent); }
#mn-16-t2:checked ~ .mn-16__navbar label[for="mn-16-t2"] .mn-16__nav-label { color: var(--accent2); }
#mn-16-t3:checked ~ .mn-16__navbar label[for="mn-16-t3"] .mn-16__nav-label { color: var(--accent3); }
#mn-16-t4:checked ~ .mn-16__navbar label[for="mn-16-t4"] .mn-16__nav-label { color: var(--accent4); }

#mn-16-t1:checked ~ .mn-16__navbar label[for="mn-16-t1"] .mn-16__nav-icon,
#mn-16-t2:checked ~ .mn-16__navbar label[for="mn-16-t2"] .mn-16__nav-icon,
#mn-16-t3:checked ~ .mn-16__navbar label[for="mn-16-t3"] .mn-16__nav-icon,
#mn-16-t4:checked ~ .mn-16__navbar label[for="mn-16-t4"] .mn-16__nav-icon {
  transform: scale(1.15);
}

/* Dot stat row */
.mn-16__stat-row { display: flex; gap: 12px; }
.mn-16__stat-pill {
  flex: 1;
  background: var(--bg);
  border-radius: 14px;
  padding: 14px 12px;
  text-align: center;
  box-shadow: 4px 4px 10px var(--shadow-dark), -4px -4px 10px var(--shadow-light);
}
.mn-16__stat-pill .val { font-size: 20px; font-weight: 700; color: var(--text); }
.mn-16__stat-pill .lbl { font-size: 10px; color: var(--muted); margin-top: 2px; }

@media (prefers-reduced-motion: reduce) {
  .mn-16__page, .mn-16__nav-icon, .mn-16__nav-label { transition: none; }
}

How this works

Neumorphism is achieved with paired box-shadow values: a light shadow offset top-left (-6px -6px 14px rgba(255,255,255,0.8)) and a dark shadow offset bottom-right (6px 6px 14px rgba(163,177,198,0.7)) — both derived from the base colour #e0e5ec. The active tab inverts this with the inset keyword on both shadows, creating a pressed-in appearance.

The same shadow technique is used for cards (.mn-16__neu-card), inset wells (.mn-16__neu-inset), icon buttons, and the progress track. Each element either protrudes or recesses from the surface. Radio inputs switch pages with the same opacity + translateY pattern used across other demos in this collection.

Customize

  • Change the base surface colour by editing --bg: #e0e5ec and updating both shadow colours — --shadow-light should be ~20% lighter than bg and --shadow-dark ~20% darker.
  • Increase shadow depth by raising the shadow spread from 14px to 20px and the blur offset from 6px to 10px on .mn-16__neu-card.
  • Add colour to the active tab by wrapping the emoji in a span and applying a filter: hue-rotate() on the active label state.
  • Make the bottom bar neumorphic by adding the same paired box-shadow to .mn-16__navbar and removing the existing box-shadow: 0 -2px 20px.
  • Use SVG icons instead of emoji by replacing the emoji inside .mn-16__nav-icon with inline SVGs; set fill: var(--muted) by default and fill: var(--accent) on the active label.

Watch out for

  • Neumorphic shadows only look correct when the element background matches the page background exactly — any transparency or different colour breaks the illusion.
  • The light and dark shadow colours are derived from the base colour — you must manually tune both shadow rgba values when changing the base; CSS cannot auto-calculate them.
  • Very high contrast or dark backgrounds break neumorphism visually — this design language is specifically tied to mid-range light grays; do not attempt it on black or saturated backgrounds.

Browser support

ChromeSafariFirefoxEdge
60+ 12+ 55+ 60+

Inset box-shadow is universally supported. No experimental features used. Works across all modern browsers without prefixes.

Search CodeFronts

Loading…