Light Mode Neumorphism: Making Soft UI Work on White Backgrounds
Light mode neumorphism looks great in theory, but breaks on white backgrounds without the right shadow math. Here's how to actually make soft UI work in 2026.
Why Light Mode Neumorphism Is Harder Than It Looks
Honestly, neumorphism on a pure white background almost never works — and most devs find out the hard way after spending an afternoon tweaking box-shadow values that never quite look right. The problem isn't the style itself. It's the math behind it.
Neumorphism depends on dual shadows: one light, one dark, both offset in opposite directions. On a mid-grey background like #e0e0e0, that contrast is easy to achieve. On white (#ffffff), the light shadow becomes invisible and you're left with only half the effect. The whole illusion collapses.
This isn't a reason to abandon the style entirely. If you understand what neumorphism actually is, you know the technique was always designed around a specific background luminosity range. You just need to work within that range, even in so-called 'light mode'.
The fix is counterintuitive: your 'light mode' neumorphic background can't be white. It has to be a very light grey — typically between #e8e8e8 and #f0f0f0. Everything else flows from that one constraint.
The Shadow Formula: Getting the Numbers Right
Let's talk specifics. A neumorphic element raised from its surface needs two shadows. The dark shadow sits on the bottom-right (or wherever your light source is pointing away from). The light shadow sits on the top-left. Both use the same base background color, shifted in opposite luminosity directions.
For a background of #e8eaf0, a solid starting point is box-shadow: 6px 6px 12px rgba(166, 168, 179, 0.5), -6px -6px 12px rgba(255, 255, 255, 0.9). That 6px offset is deliberate — too small and the effect is invisible, too large and it looks like a drop shadow from 2012. The 12px blur radius keeps it soft without going muddy.
The ratio matters too. If both shadow values are equal strength, the element looks flat again. The light shadow should generally be stronger than the dark one — something like 0.9 opacity vs 0.5. That asymmetry is what creates the sense of depth. It's one of those things you won't read in most CSS tutorials but you'll feel immediately when you see it working.
And inset shadows flip this for pressed/active states. box-shadow: inset 4px 4px 8px rgba(166, 168, 179, 0.5), inset -4px -4px 8px rgba(255, 255, 255, 0.9) makes the element look pushed into the surface. Toggle between the two on :active and you've got a button that actually feels tactile.
Building a Neumorphic Button in React + Tailwind
Tailwind v4.0.2 introduced arbitrary value support that makes neumorphic shadows a lot less painful. You don't need a separate CSS file for every shadow variant. You can define the shadow as a design token or just drop it inline with the shadow-[...] arbitrary syntax.
Here's a complete neumorphic button component that handles resting, hover, and active states — all in one TSX file:
import { ButtonHTMLAttributes } from 'react';
interface NeumorphicButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
variant?: 'raised' | 'flat';
}
export function NeumorphicButton({
children,
variant = 'raised',
className = '',
...props
}: NeumorphicButtonProps) {
const base =
'rounded-2xl px-6 py-3 text-sm font-medium text-slate-600 transition-all duration-150 select-none outline-none';
const raised =
'bg-[#e8eaf0] shadow-[6px_6px_12px_rgba(166,168,179,0.5),-6px_-6px_12px_rgba(255,255,255,0.9)]' +
' hover:shadow-[4px_4px_8px_rgba(166,168,179,0.5),-4px_-4px_8px_rgba(255,255,255,0.9)]' +
' active:shadow-[inset_4px_4px_8px_rgba(166,168,179,0.5),inset_-4px_-4px_8px_rgba(255,255,255,0.9)]';
const flat =
'bg-[#e8eaf0] shadow-[inset_4px_4px_8px_rgba(166,168,179,0.5),inset_-4px_-4px_8px_rgba(255,255,255,0.9)]';
return (
<button
className={`${base} ${variant === 'raised' ? raised : flat} ${className}`}
{...props}
>
{children}
</button>
);
}Notice the background color on the button matches the page background (#e8eaf0). This is not optional. If the button background differs from the page background, the shadows will look detached — like a regular card with drop shadows, not a neumorphic element extruding from the surface.
Accessibility Is the Real Challenge With Soft UI
Here's something that gets skipped in most neumorphism tutorials: it fails WCAG contrast requirements by design. The soft colors and low-contrast shadows are exactly what makes it look good — and exactly what makes it inaccessible to users with low vision.
The text inside neumorphic components needs to carry the contrast load that the visual design deliberately avoids. Use text-slate-700 or darker for body text on a #e8eaf0 background. Run your actual contrast ratio — tools like the WebAIM contrast checker are free and fast. You want at least 4.5:1 for normal text, 3:1 for large text.
Focus rings are another pain point. Neumorphic surfaces eat default browser focus outlines. You'll need to explicitly add :focus-visible styles that stand out. Something like focus-visible:ring-2 focus-visible:ring-slate-500 focus-visible:ring-offset-2 at minimum. Don't skip this because the shadows look clean without it — keyboard users still need to navigate your UI.
If accessibility is a hard requirement for your project, it's worth reading the glassmorphism vs neumorphism comparison — glassmorphism tends to handle accessibility slightly better due to higher background contrast ranges. Neither is an accessibility-first style, but one is worse than the other.
CSS Custom Properties Make Neumorphic Theming Maintainable
Hardcoding shadow values in every component is how you end up with a codebase nobody wants to touch. The shadow formula for neumorphism involves at least four values (two colors, two offsets) that all need to stay in sync. Custom properties solve this cleanly.
:root {
--nm-bg: #e8eaf0;
--nm-shadow-dark: rgba(166, 168, 179, 0.5);
--nm-shadow-light: rgba(255, 255, 255, 0.9);
--nm-offset: 6px;
--nm-blur: 12px;
--nm-shadow-raised:
var(--nm-offset) var(--nm-offset) var(--nm-blur) var(--nm-shadow-dark),
calc(-1 * var(--nm-offset)) calc(-1 * var(--nm-offset)) var(--nm-blur) var(--nm-shadow-light);
--nm-shadow-inset:
inset var(--nm-offset) var(--nm-offset) var(--nm-blur) var(--nm-shadow-dark),
inset calc(-1 * var(--nm-offset)) calc(-1 * var(--nm-offset)) var(--nm-blur) var(--nm-shadow-light);
}
.nm-raised { box-shadow: var(--nm-shadow-raised); background: var(--nm-bg); }
.nm-inset { box-shadow: var(--nm-shadow-inset); background: var(--nm-bg); }With this setup, changing the shadow intensity site-wide is a single token edit. It also makes it trivial to expose the shadow offset as a user preference — some users find larger offsets easier to perceive as depth. You could even wire --nm-offset to a range input if you're building a settings panel. That's the kind of thing that turns a visual style into a real design system rather than a collection of one-off components.
When to Use Neumorphism (and When to Walk Away)
Neumorphism fits specific contexts well: dashboards with lots of white space, settings panels, music players, any UI where the tactile metaphor adds value. It does not fit content-heavy pages, data tables, or anything where the user needs to scan quickly. The low contrast that makes it beautiful also makes it slow to read.
It also ages fast if you're not careful. What looked fresh in 2020 can look dated by 2026 if it's implemented without restraint. The devs who are still using this style effectively are mixing it with flat elements — neumorphic surfaces for interactive controls, flat typography for content. Full-neumorphic UIs where everything has dual shadows tend to feel overwhelming.
Compare it to claymorphism if you want a softer, more playful 3D look that's slightly more forgiving with backgrounds. Or look at neobrutalism if your brand can handle high contrast and strong borders — it's the stylistic opposite but solves the accessibility problem that neumorphism struggles with.
If you're building a theme toggle in React, be careful about how you handle neumorphism in dark mode. The technique breaks on dark backgrounds for different reasons — the light shadow becomes impossible without glowing, which is a different visual effect entirely. Most teams pick one or the other rather than trying to make neumorphism work in both modes.
Neumorphic Input Fields: The Hardest Element to Get Right
Buttons and cards are easy. Input fields are where neumorphism gets genuinely difficult. The inset shadow style communicates 'this is a container you can put things in', which is semantically correct — but the low contrast between the input background and its border-equivalent shadow makes it hard for users to find the field in the first place.
The approach that works: use the inset shadow as the unfocused state, and switch to a flat background with a colored border or highlight on :focus. This breaks the neumorphic purity but keeps the UI usable. Something like border-2 border-transparent focus:border-slate-400 alongside the inset shadow, toggled via JavaScript on focus, gives you the best of both.
Don't try to neumorphicize placeholder text. Neumorphic surfaces already have contrast problems; adding grey placeholder text on a grey background with grey shadows is genuinely unusable. Placeholder text at text-slate-400 or darker on a #e8eaf0 surface sits right at the WCAG AA boundary — check the actual number before shipping.
Putting It Together: A Neumorphic Card Component
Cards are probably the most-used neumorphic element. Here's a reusable version that takes an optional pressed prop for toggling between raised and inset states — useful for toggle cards, selectable items, anything where the user needs feedback that something is selected.
interface NeumorphicCardProps {
children: React.ReactNode;
pressed?: boolean;
className?: string;
onClick?: () => void;
}
export function NeumorphicCard({
children,
pressed = false,
className = '',
onClick,
}: NeumorphicCardProps) {
const shadow = pressed
? 'shadow-[inset_6px_6px_12px_rgba(166,168,179,0.5),inset_-6px_-6px_12px_rgba(255,255,255,0.9)]'
: 'shadow-[6px_6px_12px_rgba(166,168,179,0.5),-6px_-6px_12px_rgba(255,255,255,0.9)]';
return (
<div
onClick={onClick}
className={`rounded-3xl bg-[#e8eaf0] p-6 transition-shadow duration-200 ${shadow} ${
onClick ? 'cursor-pointer' : ''
} ${className}`}
>
{children}
</div>
);
}The transition-shadow duration-200 on the container gives you a smooth toggle between raised and inset states. 200ms is about right — fast enough to feel responsive, slow enough that the shadow transition is visible. Go below 100ms and the shadow appears to jump rather than animate.
One last thing worth noting: border-radius matters a lot for neumorphism. The rounded-3xl (24px) in the example above is close to the minimum you'd want. Sharp corners fight the soft shadow effect. Neumorphism and border-radius are basically married to each other — go lower than rounded-xl (12px) and you'll start losing the softness that makes the style work.
FAQ
Pure white (#ffffff) makes the light shadow in the dual-shadow pair invisible — you can't cast a lighter shadow on white. Your background needs to be a light grey, typically between #e0e0e0 and #f0f0f0, for both shadows to be visible and create the depth illusion.
A solid starting value for a #e8eaf0 background: box-shadow: 6px 6px 12px rgba(166, 168, 179, 0.5), -6px -6px 12px rgba(255, 255, 255, 0.9). Adjust the offset (6px) and blur (12px) proportionally to your element size — larger elements need larger offsets.
Not out of the box. The low-contrast visual style conflicts with WCAG contrast requirements. You need to compensate with darker text (slate-700 or darker on light backgrounds), explicit focus-visible rings, and careful color choices. Check actual contrast ratios with a tool like WebAIM rather than eyeballing it.
Yes, using Tailwind v4's arbitrary value syntax: shadow-[6px_6px_12px_rgba(166,168,179,0.5),-6px_-6px_12px_rgba(255,255,255,0.9)]. It's verbose but works without any plugin. If you're using the same shadow in many places, define it as a CSS custom property and reference it instead.
It's genuinely difficult. The light shadow becomes a glow effect on dark surfaces, which is a different visual language. Most teams choose not to neumorphize dark mode at all — they switch to flat or glassmorphism for dark mode and keep neumorphism for light mode only. A theme toggle with conditional shadow values is the cleanest approach if you need both.
Minimum 12px (Tailwind's rounded-xl), with 16-24px (rounded-2xl to rounded-3xl) being most common. Neumorphism relies on soft, rounded forms to complement the shadow style. Sharper corners create visual tension with the soft shadows and undermine the effect.