Skip to content
← components

Neumorphic Toggle

new

A soft, pressed-in segmented toggle — a gradient pill with two opposing inset shadows and a raised thumb that slides between sun and moon. Pure CSS.

sun / moon
view: grid

click either side — the thumb slides

Usage

example.tsx
import { NeuToggle } from "~/components/toggle/neu-toggle";
// Sun / moon by default
<NeuToggle value={theme} onChange={setTheme} aria-label="Theme" />
// Or your own two options
<NeuToggle
options={[
{ value: "list", icon: <List />, label: "List" },
{ value: "grid", icon: <LayoutGrid />, label: "Grid" },
]}
defaultValue="grid"
/>

How the softness works

It started as a Sketch recipe: a pill with a left-to-right gradient, then two inset shadows pointing opposite ways. One dark (#171717) pushed in from one side, one light (#494949) from the other. Those opposing shadows are the whole neumorphic trick. The surface looks like it was pressed out of a single soft material instead of drawn with a border.

I added the part the static recipe leaves out: a raised thumb that actually moves. It's a rounded knob with its own drop shadow and a top highlight, and it slides between the two halves with a translateX on a springy ease. The icons sit above it, so whichever one the thumb is under reads as bright and the other dims back. Keyboard and screen readers get a real radiogroup.

neu-toggle.css
/* The track: gradient + two opposing inset shadows = the soft pressed look. */
.neu-toggle {
border-radius: 100px;
background: linear-gradient(90deg, #4b4b4b, #111111);
box-shadow:
inset 20px 0 30px -10px #171717, /* dark side */
inset -20px 0 30px -10px #494949, /* light side */
0 1px 2px rgba(0, 0, 0, 0.5);
}
/* The raised knob, sliding between halves. */
.neu-toggle__thumb {
position: absolute; top: 6px; left: 6px;
width: calc(50% - 6px); height: calc(100% - 12px);
border-radius: 100px;
background: linear-gradient(180deg, #3c3c3c, #1b1b1b);
box-shadow: 0 2px 6px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.1);
transition: transform .28s cubic-bezier(.2,.7,.2,1);
}
.neu-toggle[data-index="1"] .neu-toggle__thumb { transform: translateX(100%); }

Props

proptypedefaultdescription
options[Option, Option]sun / moonExactly two options: { value, icon, label }.
valuestringControlled selected value.
defaultValuestringoptions[0]Uncontrolled initial value.
onChange(value) => voidFires on select.
aria-labelstring"Toggle"Label for the radiogroup.