Do it accessibly
Keep the softness. Fix the rest.
Strong text contrast, a real label on every control, a focus ring you can't miss, states in words and in code, and a border for people whose browsers strip shadows away.
A working example
Try it
Tab to each control to see the two-color focus ring. The toggle says On or Off in words, and tells screen readers with aria-pressed, so nobody has to spot a pressed-in shadow.
.soft-btn {
background: #e4e9f0;
color: #1c2330; /* 12.9:1 on the surface */
border: 2px solid transparent; /* becomes visible in forced colors */
border-radius: 14px;
box-shadow: 6px 6px 12px #b3bdcc, -6px -6px 12px #ffffff;
}
.soft-btn:active,
.soft-toggle[aria-pressed="true"] {
box-shadow: inset 4px 4px 8px #b3bdcc, inset -4px -4px 8px #ffffff;
}
.soft-btn:focus-visible {
outline: 3px solid #0b1f33; /* dark ring */
outline-offset: 3px;
box-shadow: 0 0 0 3px #ffffff; /* light ring: C40 two-color focus */
}
@media (forced-colors: active) {
.soft-btn { border-color: ButtonText; } /* shadows are removed here */
}
The techniques, with sources
-
box-shadow takes a list
The CSS box-shadow property accepts several shadows separated by commas. Each one has X and Y offsets, optional blur and spread radius, and a color. That's how a light and a dark shadow get stacked on one element.
Source: MDN, box-shadow
-
inset makes the pressed look
Adding the inset keyword draws the shadow inside the element instead of outside, so the element looks pressed in. Neumorphic designs use this for pressed or sunken states.
Source: MDN, box-shadow
-
A two-color focus ring works on any background
W3C technique C40 uses a focus indicator made of one light and one dark color. If the two have at least 9:1 contrast with each other, at least one meets 3:1 against any solid background. W3C notes the guarantee only holds over a single solid color, not gradients or images.
Source: W3C WAI, Technique C40
-
Expose pressed state in code, not only shadows
For toggle buttons, the aria-pressed attribute tells assistive technology whether the button is pressed. That way the state doesn't depend only on an inset shadow someone has to see.
Source: MDN, ARIA aria-pressed attribute
-
Forced colors removes every shadow
In forced colors modes like Windows High Contrast, browsers force box-shadow to none. MDN's own example is a button that relies on box-shadow and gets a solid ButtonText border inside @media (forced-colors: active).
Source: MDN, forced-colors media feature
-
prefers-contrast detects a request for more
The prefers-contrast media feature detects when someone has asked their system for more or less contrast. Its value "more" lets a page swap soft outlines for stronger ones, like MDN's dashed-to-solid outline example.
-
Add text color, a border or an icon
Because a neumorphic button can't stand out with a different background color, CSS-Tricks suggests adding weight another way: a stronger text color, a border below the text, or an icon.
-
Save it for cards and containers
CSS-Tricks recommends using neumorphism mainly on cards and other static containers that don't have interaction or validation states. It also suggests using it sparingly next to another design system.
A quick checklist
- Text is at least 4.5:1 against its surface (3:1 for large text).
- Every control has a visible text label or a high-contrast icon, not just a shadow.
- Focus shows as a two-color ring, not a glow.
- Pressed and selected states say so in words and with aria-pressed or aria-selected.
- In forced-colors mode, controls get a solid border.
- Soft shadows go on cards and containers first, controls second.