Five CSS Techniques for Checkbox States Beyond the Default
Checkboxes are one of those controls we rarely leave alone — they’re a natural spot to add a bit of flair. Yet most implementations stop at swapping a background color or toggling a border. That’s fine, but CSS has much more to offer for indicating a checked or unchecked state without requiring any complex design work.
The demos below all share the same underlying structure: the actual <input type="checkbox"> sits at the bottom of a CSS Grid stack, with two pseudo-elements (or other elements) layered above it. Only one of those layers is visible at a time, depending on the state. Every element stacked above the checkbox uses pointer-events: none so it never blocks clicks or taps on the input itself.
Blended Backgrounds as a State Indicator
Blend modes aren’t just for hero images. You can use them to change the look of a checkbox purely through color interaction. Here, two pseudo-elements — one green, one blue — are stacked over the checkbox, each given a mix-blend-mode value. The demo uses hard-light, which behaves like multiply or screen depending on whether the top color is darker or lighter than what’s behind it.
<input id="un" type="checkbox"> <label for="un">un</label>
<!-- more checkboxes -->
input[type=checkbox]::before,
input[type=checkbox]::after {
mix-blend-mode: hard-light;
pointer-events: none;
/* more style */
}
input[type=checkbox]::before {
background: green;
content: '✓';
color: white;
/* more style */
}
input[type=checkbox]::after {
background: blue;
content: '⨯';
/* more style */
}
input[type=checkbox]:checked::after {
mix-blend-mode: unset;
color: transparent;
}
When the checkbox is checked, the ::after element’s blend mode is set to its initial value, producing a noticeably different visual — a simple, declarative state change with zero transitions required.
Rotating a 3D Box for a State Toggle
With transform-style: preserve-3d on a wrapper element following the checkbox, its children can be arranged in simulated three-dimensional space. In this demo, two <i> elements — one grey, one blue — are positioned along the z-axis, with two more wedged between them to cover the left and right sides. The result is a cardboard-box-like shape that’s open at the top and bottom.
<div class="c-checkbox">
<input type="checkbox" id="un">
<!-- cube design -->
<div><i></i><i></i><i></i><i></i></div>
</div>
<label for="un">un</label>
<!-- more checkboxes -->
.c-checkbox > div {
transition: transform .6s cubic-bezier(.8, .5, .2, 1.4);
transform-style: preserve-3d;
pointer-events: none;
/* more style */
}
/* front face */
.c-checkbox > div > i:first-child {
background: #ddd;
transform: translateZ( -10px );
}
/* back face */
.c-checkbox > div > i:last-child {
background: blue;
transform: translateZ( 10px );
}
/* side faces */
.c-checkbox > div > i:nth-of-type(2),
.c-checkbox > div > i:nth-of-type(3) {
transform: rotateX(90deg)rotateY(90deg);
position: relative;
height: 20px;
top: 10px;
}
.c-checkbox > div > i:nth-of-type(2) {
background: navy;
right: 20px;
}
.c-checkbox > div > i:nth-of-type(3) {
background: darkslategray;
left: 20px;
}
On :checked, the box rotates sideways via a transform, and because the wrapper already has a transition, the rotation animates smoothly — effectively flipping a cube to reveal a different color as the state marker.
input:checked + div {
transform: rotateY( 180deg );
}
Border Radius That Affects Neighbor Boxes
Changing the border radius of just one checkbox is mildly interesting. Changing the radius of the boxes around it — on both sides — is more interesting. That requires working around CSS’s lack of a “previous sibling” selector.
<input type="checkbox" id="un"> <label for="un">un</label>
<!-- more rows of checkboxes -->
input {
background: #ddd;
border-radius: 20px;
/* more style */
}
input:not(:first-of-type)::before {
content: '';
transform: translateY(-60px); /* move up a row */
pointer-events: none;
}
input:checked + * + input::before,
input:last-of-type:checked {
border-radius: 20px;
background: blue;
}
input:checked + * + input:checked + * + input::before {
border-top-left-radius: unset !important;
border-top-right-radius: unset !important;
}
input:checked::before {
border-bottom-left-radius: unset !important;
border-bottom-right-radius: unset !important;
}
/* between the second-last and last boxes */
input:nth-of-type(4):checked + * + input:checked {
border-top-left-radius: unset;
border-top-right-radius: unset;
}
The technique uses pseudo-elements to reach backward. Each checkbox (except the first) carries a pseudo-element that is visually placed on top of the box preceding it. So for a row of boxes A, B, C, and D, clicking B lets you style A through B’s pseudo-element, B through C’s pseudo-element, and C through D’s pseudo-element — as long as the next-sibling selector (+ or ~) can traverse the layout.
The demo initially rounds all four corners of every box. When one is checked, the top corners of the following box and the bottom corners of the preceding box are straightened, producing a connected, accordion-like effect.
Masked Toggles That Reveal Backgrounds
Toggle switches are just checkboxes with different clothes. This technique uses CSS masking — filtering portions of an element using an image or gradient — to build a switch-like control. Two landscape-photo layers sit atop a checkbox; the top layer carries a mask-image in the shape of a toggle: an opaque background with a transparent circle on the left.
<input type="checkbox">
<div class="skin one"></div>
<div class="skin two"></div>
.one.skin {
background: no-repeat center -40px url('photo-1584107662774-8d575e8f3550?w=350&q=100');
}
.two.skin {
background: no-repeat center -110px url('photo-1531430550463-9658d67c492d?w=350&q=100');
--mask: radial-gradient(circle at 45px 45px , rgba(0,0,0,0) 40px, rgba(0,0,0,1) 40px);
mask-image: var(--mask); -webkit-mask-image: var(--mask);
}
When checked, the transparent circle shifts right. The top photo shows through the circle; the area outside it displays the lower photo. The mask image handles all of the reveal logic without extra markup or JavaScript.
input:checked ~ .two.skin {
--mask: radial-gradient(circle at 305px 45px, rgba(0,0,0,1) 40px, rgba(0,0,0,0) 40px);
mask-image: var(--mask); -webkit-mask-image: var(--mask);
}
Animated Inset Box Shadows
Finally, the simplest trick of the set — and arguably the most reliable: an animated inset box-shadow. Since box-shadow is an animatable property, you can smoothly transition from a delicate outline to a filled-looking state by expanding an inner shadow. It reads as elegant and calm, which suits minimalist interfaces well.
<input id="un" type="checkbox"> <label for="un">un</label>
input {
transition: box-shadow .3s;
background: lightgrey;
/* more style */
}
input:checked {
box-shadow: inset 0 0 0 20px blue;
}


