EmpireUI
Get Pro
← Blog8 min read#tailwind#neumorphism#soft ui

Neumorphism in Tailwind CSS: Soft Shadows Without the Opacity Trap

Build real neumorphic UI in Tailwind CSS without breaking dark mode or accessibility. Practical shadow configs, Tailwind plugin tricks, and pitfalls to avoid.

Soft neumorphic UI card with light shadow depth on pale gray surface

What Neumorphism Actually Is (and Why It's Still Tricky in 2026)

Neumorphism is the design style where UI elements look like they're extruded from or pressed into the surface they sit on — not floating above it. The trick is two shadows: one bright highlight thrown from a top-left light source, and one darker shadow cast bottom-right. The element and its background share the exact same color. That's the entire mechanic.

It blew up around 2020 when Michal Malewicz published his Dribbble shots and every designer on the internet lost their mind. Six years later it's still showing up in dashboards, music players, and smart-home apps because that tactile, physical quality is genuinely hard to replicate with flat design. You just have to do it carefully or it becomes an accessibility disaster.

The opacity trap is the thing that trips most devs. You've seen it — someone writes box-shadow: 6px 6px 12px rgba(0,0,0,0.15), -6px -6px 12px rgba(255,255,255,0.7) and it looks fine on their beige MacBook display. Open it on a Samsung with a cooler color profile, or switch to dark mode, and it falls apart completely. Hardcoded opacity values don't adapt. The fix isn't to avoid opacity — it's to anchor your shadows to the background color, not to abstract white/black.

Honestly, the best neumorphic UIs you'll see in production don't go overboard. Three or four interactive elements max. The rest is flat. If you're building a full neumorphism design system, pick your battle surface and stay disciplined.

The Shadow Math: How Neumorphic Shadows Actually Work

You need two box-shadow layers. The highlight should be a lighter tint of your background — not white. The dark shadow should be a darker shade — not black. If your surface is #e0e5ec, your highlight is something like #ffffff or #f0f4fa, and your dark shadow is #a3b1c6. The offset, blur, and spread all matter.

A solid starting formula for a raised element at 12px offset: ``css .neu-raised { background: #e0e5ec; box-shadow: 12px 12px 24px #a3b1c6, -12px -12px 24px #ffffff; } ` For a pressed/inset state — which is what you'd use for active buttons or inputs: `css .neu-pressed { background: #e0e5ec; box-shadow: inset 6px 6px 12px #a3b1c6, inset -6px -6px 12px #ffffff; } ``

The offset should roughly equal half the blur radius. Push offsets too large relative to blur and the shadows look disconnected. Keep them too small and the effect flattens out. Worth noting: at a 16px blur with an 8px offset you get a very gentle, almost ambient effect. At 24px blur with 12px offset you get something more dramatic and sculptural.

Quick aside: the spread value (the fourth number in box-shadow) is usually 0 for neumorphism. Adding a positive spread value makes shadows bleed further out and can eat into adjacent elements. Keep it 0 unless you're going for something intentionally diffused.

Wiring It Into Tailwind: Plugin vs. Arbitrary Values

Tailwind's default shadow-* utilities don't cover neumorphism — they're single-direction, black-tinted shadows. You've got two routes: arbitrary values inline, or a proper tailwind.config.js extension.

For one-off use, arbitrary values work fine in Tailwind v3.4+: ``html <div class="bg-[#e0e5ec] [box-shadow:12px_12px_24px_#a3b1c6,-12px_-12px_24px_#ffffff] rounded-2xl p-6"> <!-- content --> </div> `` That gets messy fast. The underscore-for-space escaping is annoying and the class names are unreadable in your JSX.

The better move is to extend your tailwind.config.js with named shadows: ``js // tailwind.config.js module.exports = { theme: { extend: { boxShadow: { 'neu-raised': '12px 12px 24px #a3b1c6, -12px -12px 24px #ffffff', 'neu-pressed': 'inset 6px 6px 12px #a3b1c6, inset -6px -6px 12px #ffffff', 'neu-flat': '4px 4px 8px #a3b1c6, -4px -4px 8px #ffffff', }, }, }, } ` Now you write shadow-neu-raised and hover:shadow-neu-pressed` like any other Tailwind utility. Clean, readable, and your designer can actually read the config.

In practice, you'll also want a CSS variable approach if you're supporting multiple themes. Lock the shadow colors to CSS custom properties and swap them per theme: ``css :root { --neu-shadow-dark: #a3b1c6; --neu-shadow-light: #ffffff; --neu-bg: #e0e5ec; } [data-theme='dark'] { --neu-shadow-dark: #1a1d23; --neu-shadow-light: #2e3340; --neu-bg: #252830; } ` Then in your Tailwind config reference those variables: `js boxShadow: { 'neu-raised': '12px 12px 24px var(--neu-shadow-dark), -12px -12px 24px var(--neu-shadow-light)', } ``

One more thing — Tailwind's JIT engine will purge any class that doesn't appear literally in your source. If you're generating class names dynamically (e.g. shadow-neu-${state}), you need to safelist those in your config or use CSS directly.

Dark Mode Neumorphism: The Part Everyone Gets Wrong

Look, dark mode neumorphism is genuinely hard and most implementations you find online are garbage. The wrong approach is inverting the shadows — dark surface, then white highlight on top-left and near-black on bottom-right. That creates a floating-glowing-orb effect, not an extruded surface.

The correct approach: dark surface, slightly lighter highlight (not white — maybe 20% lighter than the surface), slightly darker shadow (maybe 30-40% darker). Here's a dark-mode config that actually looks correct: ``css /* Dark base: #252830 */ .neu-dark-raised { background: #252830; box-shadow: 12px 12px 24px #1a1d23, -12px -12px 24px #303540; } `` The contrast ratio between highlight and shadow is tighter in dark mode — that's intentional. Dark surfaces can't handle the same drama as light ones.

Another gotcha: don't use dark:shadow-* Tailwind variants if your shadow colors are hardcoded. The variant applies to the class but the colors inside the shadow string don't adapt. You need separate named shadow utilities for dark mode, or you need CSS variables (see the approach above).

If you're building something that needs to look polished across both modes, take a look at how Empire UI's neumorphism components handle theme switching — the implementation is a useful reference even if you're rolling your own.

Accessibility: Neumorphism's Actual Achilles Heel

Here's the uncomfortable truth: default neumorphic design has terrible contrast. You're relying on shadows and depth cues instead of color contrast to communicate that something is a button. WCAG 2.1 AA requires a 3:1 contrast ratio for UI components against adjacent colors. Shadows don't count.

The fix isn't abandoning the style — it's layering. Keep the neumorphic shadows for the dimensional quality, but add a visible label with sufficient text contrast (4.5:1 for normal text), and make sure the interactive boundary is obvious enough without relying solely on the shadow. A 1px border in a slightly darker shade of the background — something like border: 1px solid #c8d0de on a #e0e5ec surface — does a lot without ruining the aesthetic.

Focus indicators are the other thing that gets axed in neumorphic UIs because designers don't want to break the visual flow. Don't do that. Add a focus ring that's clearly visible: ``css .neu-button:focus-visible { outline: 3px solid #4f6ef7; outline-offset: 4px; } `` That 4px offset keeps the ring outside the shadow area so it doesn't look janky.

Real talk: neumorphism works best when it's applied to non-critical UI — volume sliders, toggle switches, decorative cards. For primary CTAs and form inputs that users depend on to navigate, lean on contrast, not depth.

Complete Component Example: Neumorphic Toggle Switch

A toggle switch is the canonical neumorphic component because the pressed/raised state change maps perfectly to on/off. Here's a full working React + Tailwind implementation: ``tsx import { useState } from 'react' export function NeuToggle() { const [on, setOn] = useState(false) return ( <button role="switch" aria-checked={on} onClick={() => setOn(!on)} className={ relative w-16 h-8 rounded-full bg-[#e0e5ec] transition-shadow duration-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 ${ on ? '[box-shadow:inset_6px_6px_12px_#a3b1c6,inset_-6px_-6px_12px_#ffffff]' : '[box-shadow:6px_6px_12px_#a3b1c6,-6px_-6px_12px_#ffffff]' } } > <span className={ absolute top-1 w-6 h-6 rounded-full bg-[#e0e5ec] [box-shadow:3px_3px_6px_#a3b1c6,-3px_-3px_6px_#ffffff] transition-transform duration-200 ${ on ? 'translate-x-9' : 'translate-x-1' } } /> </button> ) } ``

That gives you a toggle that animates between raised (off) and inset (on) states with a smooth 200ms transition. The knob itself has its own neumorphic shadow so it maintains that extruded feel even when the track is pressed.

Quick aside: if you want to add this to a Tailwind config as proper named utilities instead of inline arbitrary values, extract the box-shadow strings into your tailwind.config.js using the approach from the previous section. Your JSX will thank you.

You can extend this pattern to card components, sliders, and input fields. The key is always the same: raised state gets outset shadows, active/pressed state gets inset. That single rule covers 90% of neumorphic interactions. Compare how this feels against the frosted aesthetic of glassmorphism components — they solve the same depth problem from completely different angles.

Performance and When to Skip It Entirely

Multiple box-shadow layers have a rendering cost, especially when you're animating them. A single transition between two multi-layer box-shadow values forces the browser to composite new paint layers. On mobile, at 60fps, with a dozen neumorphic cards on screen, you'll feel it.

The optimization: use will-change: box-shadow only on elements you're actually animating, not globally. Better yet, animate opacity on a pseudo-element overlay instead of the shadow directly. Or — and this is underrated — use a static shadow and animate only the transform: ``css .neu-card { box-shadow: 8px 8px 16px #a3b1c6, -8px -8px 16px #ffffff; transition: transform 0.15s ease; } .neu-card:active { transform: scale(0.98); } ` transform is cheap. box-shadow` animation is not.

When should you skip neumorphism entirely? If your background isn't a single, consistent color — skip it. If you're working on a product with strict WCAG AAA requirements — skip it, or limit it to purely decorative elements. If you need to support IE11 — it's 2026, what are you doing?

That said, for the right context — a personal dashboard, a music player, a settings panel — neumorphism is genuinely beautiful and feels more physical than any flat design trend has managed. The style hub at Empire UI has pre-built components ready to drop in if you want a head start rather than building each shadow from scratch. It saves you the trial-and-error on the shadow values alone.

FAQ

Can I use Tailwind's built-in shadow utilities for neumorphism?

No — Tailwind's default shadows are single-directional black-tinted utilities. You need dual opposing shadows with custom colors. Extend tailwind.config.js with named boxShadow entries or use arbitrary value syntax like [box-shadow:...].

How do I make neumorphism work in dark mode?

Use CSS variables for your shadow colors and swap them per theme — don't just invert the values. Dark neumorphism uses tighter contrast between highlight and shadow than the light version does.

Is neumorphism accessible?

Not out of the box. It relies on depth cues that don't meet WCAG contrast requirements for interactive components. You need to add visible borders, sufficient text contrast, and explicit focus rings on top of the shadow effect.

Does animating neumorphic shadows hurt performance?

Yes, multi-layer box-shadow transitions are expensive to paint. Prefer animating transform or opacity instead. Reserve transition: box-shadow for subtle state changes on a small number of elements.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Neobrutalism with Tailwind: offset-y Shadows, Bold Borders, Raw TypographyTailwind Shadow System: Custom Shadows, Colored Drop Shadows, BlurNeumorphism Music Player: Soft UI Controls With Inset ShadowsNeumorphism Button Design: Soft UI Done Right