20 CSS Pagination20 / 20

Pure CSSMIT licensed

Neumorphism Soft UI Pagination

Soft UI done with the contrast problem actually solved: extruded keys from a dual shadow pair, an inset press state for the active page, and a real accent colour plus weight change on the current item — because a shadow-only affordance sits near 1.5:1 and fails WCAG on its own. Includes a forced-colors block that restores visible borders.

Published

Live Demo
Try it

The code

<section class="pgn-20" aria-label="Neumorphism Pagination with Soft-UI Dual Shadows demo">
  <div class="pgn-20__stage">
    <header class="pgn-20__head">
      <p class="pgn-20__eyebrow">Soft UI</p>
      <h2 class="pgn-20__title">Extruded, then pressed</h2>
      <p class="pgn-20__sub">One light source, top-left. Raised keys cast a dark shadow down-right and a light one up-left; the active key inverts both to sit inside the surface.</p>
    </header>

    <nav class="pgn-20__nav" aria-label="Pagination">
      <button class="pgn-20__key" type="button" aria-label="Previous page">
        <svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M12.5 4.5 7 10l5.5 5.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
      </button>
      <ol class="pgn-20__list">
        <li><button class="pgn-20__key" type="button">1</button></li>
        <li><button class="pgn-20__key" type="button">2</button></li>
        <li><button class="pgn-20__key pgn-20__key--on" type="button" aria-current="page">3</button></li>
        <li><button class="pgn-20__key" type="button">4</button></li>
        <li><button class="pgn-20__key" type="button">5</button></li>
      </ol>
      <button class="pgn-20__key" type="button" aria-label="Next page">
        <svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M7.5 4.5 13 10l-5.5 5.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
      </button>
    </nav>

    <p class="pgn-20__caption">Page 3 of 5 · 74 items</p>
  </div>
</section>
.pgn-20 {
  --bg: #e8ecf2;
  --text: #2c3444;
  --muted: #7a8497;
  --acc: #3b5bdb;
  --light: #ffffff;
  --dark: #c3cbd8;
  --ring: #3b5bdb;
  --d: .55rem;
  --blur: 1.1rem;
}

@supports (color: oklch(50% 0 0)) {
  .pgn-20 {
    --bg: oklch(93.5% .012 250);
    --text: oklch(35% .03 260);
    --muted: oklch(60% .025 258);
    --acc: oklch(52% .17 265);
    --light: oklch(100% 0 0);
    --dark: oklch(84% .018 250);
    --ring: oklch(52% .17 265);
  }
}

@media (prefers-color-scheme: dark) {
  .pgn-20:not([data-theme="light"]) {
    --bg: #252a33;
    --text: #dfe4ec;
    --muted: #98a2b3;
    --acc: #8ea2ff;
    --light: #2f3540;
    --dark: #1b1f26;
    --ring: #8ea2ff;
  }
}

.pgn-20[data-theme="dark"] {
  --bg: #252a33;
  --text: #dfe4ec;
  --muted: #98a2b3;
  --acc: #8ea2ff;
  --light: #2f3540;
  --dark: #1b1f26;
  --ring: #8ea2ff;
}

.pgn-20 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: clamp(1.5rem,5vw,4rem);
  background: var(--bg);
  color: var(--text);
  font-family: system-ui,-apple-system,"Segoe UI",sans-serif;
  -webkit-font-smoothing: antialiased;
}

.pgn-20,
.pgn-20 *,
.pgn-20 *::before,
.pgn-20 *::after {
  box-sizing: border-box;
}

.pgn-20__stage {
  width: 100%;
  max-width: 42rem;
  display: grid;
  gap: 2.25rem;
  justify-items: center;
  text-align: center;
}

.pgn-20__eyebrow {
  margin: 0 0 .45rem;
  font-size: .6875rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--muted);
}

.pgn-20__title {
  margin: 0 0 .6rem;
  font-size: clamp(1.4rem,3.4vw,2rem);
  font-weight: 600;
  letter-spacing: -.025em;
  color: var(--text);
}

.pgn-20__sub {
  margin: 0;
  max-width: 32rem;
  font-size: .875rem;
  line-height: 1.65;
  color: var(--muted);
  text-wrap: pretty;
}

.pgn-20__nav {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .9rem 1rem;
  border-radius: 1.5rem;
  background: var(--bg);
  box-shadow: var(--d) var(--d) calc(var(--blur) * 1.6) var(--dark), calc(var(--d) * -1) calc(var(--d) * -1) calc(var(--blur) * 1.6) var(--light);
}

.pgn-20__list {
  display: flex;
  gap: .6rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.pgn-20__key {
  display: grid;
  place-items: center;
  min-width: 3rem;
  height: 3rem;
  padding: 0 .5rem;
  border: 0;
  border-radius: .9rem;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: .9375rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  box-shadow: calc(var(--d) * .55) calc(var(--d) * .55) var(--blur) var(--dark), calc(var(--d) * -.55) calc(var(--d) * -.55) var(--blur) var(--light);
  transition: box-shadow .22s ease, color .22s ease, translate .22s ease;
}

.pgn-20__key svg {
  width: 1.1rem;
  height: 1.1rem;
}

.pgn-20__key:hover {
  color: var(--acc);
  translate: 0 -1px;
}

.pgn-20__key:active,
.pgn-20__key--on {
  color: var(--acc);
  translate: 0 0;
  box-shadow: inset calc(var(--d) * .5) calc(var(--d) * .5) calc(var(--blur) * .8) var(--dark), inset calc(var(--d) * -.5) calc(var(--d) * -.5) calc(var(--blur) * .8) var(--light);
}

.pgn-20__key--on {
  font-weight: 700;
}

.pgn-20__key:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 3px;
}

.pgn-20__caption {
  margin: 0;
  font-size: .8125rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

@media (forced-colors: active) {
  .pgn-20__key {
    border: 1px solid CanvasText;
  }

  .pgn-20__key--on {
    forced-color-adjust: none;
    background: Highlight;
    color: HighlightText;
  }
}

@media (prefers-reduced-motion: reduce) {
  .pgn-20 * {
    animation: none !important;
    transition: none !important;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Pagination from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Neumorphism Soft UI Pagination
Source: https://codefronts.com/navigation/css-pagination/neumorphism-soft-ui-pagination/

Soft UI done with the contrast problem actually solved: extruded keys from a dual shadow pair, an inset press state for the active page, and a real accent colour plus weight change on the current item — because a shadow-only affordance sits near 1.5:1 and fails WCAG on its own. Includes a forced-colors block that restores visible borders.
## HTML
```html
<section class="pgn-20" aria-label="Neumorphism Pagination with Soft-UI Dual Shadows demo">
  <div class="pgn-20__stage">
    <header class="pgn-20__head">
      <p class="pgn-20__eyebrow">Soft UI</p>
      <h2 class="pgn-20__title">Extruded, then pressed</h2>
      <p class="pgn-20__sub">One light source, top-left. Raised keys cast a dark shadow down-right and a light one up-left; the active key inverts both to sit inside the surface.</p>
    </header>

    <nav class="pgn-20__nav" aria-label="Pagination">
      <button class="pgn-20__key" type="button" aria-label="Previous page">
        <svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M12.5 4.5 7 10l5.5 5.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
      </button>
      <ol class="pgn-20__list">
        <li><button class="pgn-20__key" type="button">1</button></li>
        <li><button class="pgn-20__key" type="button">2</button></li>
        <li><button class="pgn-20__key pgn-20__key--on" type="button" aria-current="page">3</button></li>
        <li><button class="pgn-20__key" type="button">4</button></li>
        <li><button class="pgn-20__key" type="button">5</button></li>
      </ol>
      <button class="pgn-20__key" type="button" aria-label="Next page">
        <svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M7.5 4.5 13 10l-5.5 5.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
      </button>
    </nav>

    <p class="pgn-20__caption">Page 3 of 5 · 74 items</p>
  </div>
</section>
```
## CSS
```css
.pgn-20 {
  --bg: #e8ecf2;
  --text: #2c3444;
  --muted: #7a8497;
  --acc: #3b5bdb;
  --light: #ffffff;
  --dark: #c3cbd8;
  --ring: #3b5bdb;
  --d: .55rem;
  --blur: 1.1rem;
}

@supports (color: oklch(50% 0 0)) {
  .pgn-20 {
    --bg: oklch(93.5% .012 250);
    --text: oklch(35% .03 260);
    --muted: oklch(60% .025 258);
    --acc: oklch(52% .17 265);
    --light: oklch(100% 0 0);
    --dark: oklch(84% .018 250);
    --ring: oklch(52% .17 265);
  }
}

@media (prefers-color-scheme: dark) {
  .pgn-20:not([data-theme="light"]) {
    --bg: #252a33;
    --text: #dfe4ec;
    --muted: #98a2b3;
    --acc: #8ea2ff;
    --light: #2f3540;
    --dark: #1b1f26;
    --ring: #8ea2ff;
  }
}

.pgn-20[data-theme="dark"] {
  --bg: #252a33;
  --text: #dfe4ec;
  --muted: #98a2b3;
  --acc: #8ea2ff;
  --light: #2f3540;
  --dark: #1b1f26;
  --ring: #8ea2ff;
}

.pgn-20 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: clamp(1.5rem,5vw,4rem);
  background: var(--bg);
  color: var(--text);
  font-family: system-ui,-apple-system,"Segoe UI",sans-serif;
  -webkit-font-smoothing: antialiased;
}

.pgn-20,
.pgn-20 *,
.pgn-20 *::before,
.pgn-20 *::after {
  box-sizing: border-box;
}

.pgn-20__stage {
  width: 100%;
  max-width: 42rem;
  display: grid;
  gap: 2.25rem;
  justify-items: center;
  text-align: center;
}

.pgn-20__eyebrow {
  margin: 0 0 .45rem;
  font-size: .6875rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--muted);
}

.pgn-20__title {
  margin: 0 0 .6rem;
  font-size: clamp(1.4rem,3.4vw,2rem);
  font-weight: 600;
  letter-spacing: -.025em;
  color: var(--text);
}

.pgn-20__sub {
  margin: 0;
  max-width: 32rem;
  font-size: .875rem;
  line-height: 1.65;
  color: var(--muted);
  text-wrap: pretty;
}

.pgn-20__nav {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .9rem 1rem;
  border-radius: 1.5rem;
  background: var(--bg);
  box-shadow: var(--d) var(--d) calc(var(--blur) * 1.6) var(--dark), calc(var(--d) * -1) calc(var(--d) * -1) calc(var(--blur) * 1.6) var(--light);
}

.pgn-20__list {
  display: flex;
  gap: .6rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.pgn-20__key {
  display: grid;
  place-items: center;
  min-width: 3rem;
  height: 3rem;
  padding: 0 .5rem;
  border: 0;
  border-radius: .9rem;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: .9375rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  box-shadow: calc(var(--d) * .55) calc(var(--d) * .55) var(--blur) var(--dark), calc(var(--d) * -.55) calc(var(--d) * -.55) var(--blur) var(--light);
  transition: box-shadow .22s ease, color .22s ease, translate .22s ease;
}

.pgn-20__key svg {
  width: 1.1rem;
  height: 1.1rem;
}

.pgn-20__key:hover {
  color: var(--acc);
  translate: 0 -1px;
}

.pgn-20__key:active,
.pgn-20__key--on {
  color: var(--acc);
  translate: 0 0;
  box-shadow: inset calc(var(--d) * .5) calc(var(--d) * .5) calc(var(--blur) * .8) var(--dark), inset calc(var(--d) * -.5) calc(var(--d) * -.5) calc(var(--blur) * .8) var(--light);
}

.pgn-20__key--on {
  font-weight: 700;
}

.pgn-20__key:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 3px;
}

.pgn-20__caption {
  margin: 0;
  font-size: .8125rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

@media (forced-colors: active) {
  .pgn-20__key {
    border: 1px solid CanvasText;
  }

  .pgn-20__key--on {
    forced-color-adjust: none;
    background: Highlight;
    color: HighlightText;
  }
}

@media (prefers-reduced-motion: reduce) {
  .pgn-20 * {
    animation: none !important;
    transition: none !important;
  }
}
```

How this works

The element and its background are the same colour; depth is entirely two shadows offset in opposite directions from one light source at top-left:

box-shadow:
   .3rem  .3rem 1.1rem var(--dark),
  -.3rem -.3rem 1.1rem var(--light);

Offset and blur live in --d and --blur, so every key and the tray behind them share one physical scale. Pressing inverts the illusion by moving both shadows inside — same values, plus the inset keyword:

.pgn-20__key:active, .pgn-20__key--on {
  box-shadow: inset .27rem .27rem .88rem var(--dark),
              inset -.27rem -.27rem .88rem var(--light);
}

Because only the inset flag changes, the transition stays cheap.

Make it yours

  • Everything keys off --bg. Derive --light and --dark from it rather than picking them independently, or the material breaks.
  • Deeper relief is a larger --d; softer plastic is a larger --blur.
  • In dark mode the light source stays top-left but the surface is dark, so the two shadow colours swap roles — that is already handled in the dark token block.
  • Use the accent on text only. Filling a neumorphic key with colour destroys the illusion.

Gotchas — read before shipping

  • Contrast is this style's fatal flaw. The current page must carry a weight or colour change too — an inset shadow alone can sit at 1.5:1, far under the 3:1 non-text minimum.
  • The focus ring must be a real outline, not another shadow, or it disappears into the material.
  • The effect collapses over any patterned or gradient background; the element must sit on its own flat colour.
  • Large blur radii across many elements are a genuine paint cost on low-end mobile — keep the tray and keys, skip decorative extras.
  • A @media (forced-colors: active) block is mandatory here: with shadows suppressed, the buttons would otherwise vanish entirely.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by oklch(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.

box-shadow is universal; the floor is oklch() in the token layer (Chrome 111, Safari 16.4, Firefox 113), behind @supports over an sRGB baseline. forced-colors needs Chrome 89 / Firefox 89 and is ignored harmlessly elsewhere.

Techniques used in this demo

Search CodeFronts

Loading…