Grouped Checkbox Selection in CSS Grids
Selectable items often appear in grid layouts — calendars, galleries, file explorers, even CAPTCHA challenges. When users select multiple items in such a grid, it can be helpful to visually group adjacent selections together. One clean way to achieve this is with a combination of CSS Grid, pseudo-elements, and the :checked pseudo-class.
The core idea is straightforward: style each checkbox using the pseudo-elements of its neighboring boxes rather than its own pseudo-elements. This lets a checked box influence the appearance of adjacent checked boxes, creating a unified visual block.
How the Technique Works
The effect hinges on placing each checkbox's ::before and ::after pseudo-elements so they extend into the neighboring cells. Specifically, the ::before of boxes in non-first columns overlaps the box to the left, while the ::after of boxes in non-first rows overlaps the box above.

By default, these pseudo-elements are hidden (translated off-screen). When adjacent checkboxes are both :checked, their overlapping pseudo-elements become visible, merging the two selected cells into what looks like a single grouped object.
Base Markup and Initial Styles
The HTML structure is minimal — a grid of checkboxes:
<main>
<input type=checkbox>
<input type=checkbox>
<input type=checkbox>
<!-- more boxes -->
</main>
The grid container itself uses CSS Grid to lay out five rows and four columns:
/* The grid */
main {
display: grid;
grid: repeat(5, 60px) / repeat(4, 85px);
align-items: center;
justify-items: center;
margin: 0;
}
The checkboxes are stripped of their default appearance then given a light gray background and rounded borders:
/* all checkboxes */
input {
-webkit-appearance: none;
appearance: none;
background: #ddd;
border-radius: 20px;
cursor: pointer;
display: grid;
height: 40px;
width: 60px;
margin: 0;
}
Each checkbox is also declared as its own grid, which is essential for positioning the pseudo-elements inside it:
/* pseudo-elements except for the first column and first row */
input:not(:nth-of-type(4n+1))::before,
input:nth-of-type(n+5)::after {
content: '';
border-radius: 20px;
grid-area: 1 / 1;
pointer-events: none;
}
To avoid overlapping the edges, pseudo-elements are only defined for checkboxes not in the first column or first row. Selectors like input:not(:nth-of-type(4n+1)) identify checkboxes to skip — here, every fourth item starting from the first. The ::after elements are positioned above boxes from the fifth one onward. Both pseudo-elements start hidden, shifted left or up:
/* pseudo-elements other than the first column */
input:not(:nth-of-type(4n+1))::before {
transform: translatex(-85px);
}
/* pseudo-elements other than the first row */
input:nth-of-type(n+5)::after {
transform: translatey(-60px);
}
Checking the Boxes
When a checkbox receives the :checked state, it gets a colored background:
input:checked { background: limegreen; }
More importantly, a checked checkbox must be able to re-style the pseudo-elements of its neighbors so that grouped selections look connected:

The exact selectors depend on the grid's column count. In a 5×4 grid, adjacent checked checkboxes are handled with these rules:
/* a checked box's right borders (if the element to its right is checked) */
input:not(:nth-of-type(4n)):checked + input:checked::before {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
background: limegreen;
}
/* a checked box's bottom borders (if the element below is checked) */
input:nth-last-of-type(n+5):checked + * + * + * + input:checked::after {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
background: limegreen;
}
/* a checked box's adjacent (right side) checked box's left borders */
input:not(:nth-of-type(4n)):checked + input:checked + input::before {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
background: limegreen;
}
/* a checked box's adjacent (below) checked box's top borders */
input:not(:nth-of-type(4n)):checked + * + * + * + input:checked + input::before {
border-top-left-radius: 0;
border-top-right-radius: 0;
background: limegreen;
}
This approach works well when the number of columns is fixed and small — typical for image galleries or calendars on mobile — while rows can grow freely. If your layout has expanding columns instead, consider rotating the grid so the stream of items flows along the fixed axis.
Boundary conditions also need attention. Some cells at the grid's edges — the last column or last row — aren't covered by any pseudo-element. These require extra selectors:
/* a checked box's (in last column) left borders */
input:nth-of-type(4n-1):checked + input:checked {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
/* a checked box's (in last column) adjacent (below) checked box's top borders */
input:nth-of-type(4n):checked + * + * + * + input:checked {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
That first selector evaluates to:
A checked input next to a checked input in the second-last column.
The nth-of-type calculation advances in steps:
4(0) - 1 = no match
4(1) - 1 = 3rd item
4(2) - 1 = 7th item
4(3) - 1 = 11th item
etc.
input:nth-of-type(4n-1):checked + input:checked
An input that is checked, directly adjacent to an element which is directly adjacent to another element, which is also directly adjacent to an input that is checked.
This latter rule targets every fourth checkbox in the grid, connecting selections that are spaced along the same row.
input:nth-of-type(4n):checked + * + * + * + input:checked
Adapting the Pattern
The rounded border look is just one possibility. The same logic can create different shapes, background effects, or even varied hover states. The technique is a good demonstration of CSS combinators and pseudo-classes: styling one element based on the state of another. With a solid handle on these selectors, grid-based selection UIs can become far more expressive.



