Here’s a pretty common pattern we see while designing responsive experiences or modernizing legacy applications. If you’ve used a cloud-based file manager, content management system or administrative UI, I’m sure you’ve seen it, too.

There’s a list of content. You can edit individual items by tapping or clicking their row, or you can select adjacent checkboxes to perform bulk actions.

Some versions of this require no JavaScript, showing persistent bulk actions as form elements.

A list of posts in WordPress (Note the “bulk actions” form)But most modern interfaces wait to reveal those controls until a selection is made. We can design big, touch-friendly bulk action controls, knowing they won’t monopolize the screen until selection has started.

Selecting from a table of transactions in YNABTraditionally, that sort of thing requires JavaScript. So it’s been fun to surprise some of our customers’ development teams by delivering prototypes that mostly function with HTML and CSS alone.

DemoHere’s a simple example:

CodePen Embed FallbackAt first glance, this looks pretty ordinary. Each row links to a (hypothetical) edit page, each has its own checkbox.

But once you check a box, some interesting things happen:

  • A sticky bulk action toolbar is displayed.
  • All rows become checkbox toggles, enabling faster selection without accidental navigation.
  • The “Clear selection” button unchecks all the boxes.

Before selectionAfter selectionSo… how’s it work?

MarkupThe demo consists of a form, a heading, a list of links and labelled checkboxes, and two buttons:

```

Edit Items

`` The button withtype="reset"reverts all of the checkboxes to their initial unchecked state when clicked. The other button submits the form, which willGETorPOST` all the checked boxes.

(I affectionately call this HTML “ancient,” since there’s nothing here you won’t find in the decades-old HTML 4 specification.)

StylesThe real star of the CSS show here is the :has selector. We can combine this with :checked to detect when a selection is in progress:

.example:has(:checked) { /* do things when boxes therein are checked */} The demo uses this feature in two main ways…

Expanding the Checkbox Toggle AreaAs mentioned in the previous section, each item (or row) contains a link and a label with a nested checkbox:

```

  • (title)
  • `` Note the utility class on the` element, which lets us hide the element visually (while keeping it accessible to screen readers) using this classic technique:

    .u-hidden-visually { block-size: 1px; border: 0; clip: rect(0 0 0 0); clip-path: polygon(0 0, 0 0, 0 0); inline-size: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; white-space: nowrap;} (Spoiler: We’ll use this again in the next section.)

    We compose the remaining, visible items with CSS grid using named lines (a technique I’ve written about before). Named lines are more verbose than grid-template-areas, but I find them more intuitive when I want several areas that overlap.

    In this (slightly simplified) example, the full area overlaps both the label and link areas:

    .item { display: grid; grid-template-columns: [full-start label-start] auto [label-end link-start] minmax(0, 1fr) [link-end full-end]; grid-template-rows: [full-start label-start link-start] auto [full-end label-end link-end];} That may seem like a lot, but here’s the payoff: Now anytime there’s a checked box, we can stretch the label over the whole element by updating a single property.

    .item__label { grid-area: label;}.items:has(:checked) .item__label { grid-area: full;} Revealing Bulk ActionsBy comparison, showing the bulk action toolbar seems straightforward.

    First, we make it sticky:

    .actions { inset-block-end: 0.375em; position: sticky;} Then, we hide that element when there aren’t any checked boxes:

    .listing:not(:has(:checked)) .actions { display: none;} And we’re done, right? Well, not exactly.

    Unfortunately, display: none will hide those controls from everyone. This denies users of assistive devices important context for the form and its functions.

    So let’s do the right thing and make a couple small changes:

    • Instead of display: none, let’s piggy-back on those u-hidden-visually styles from the previous section.
    • And let’s only apply those styles here if the elements therein lack visible focus. Otherwise, keyboard users could focus invisible buttons, which seems confusing.

    .listing:not(:has(:checked)) .actions:not(:has(:focus-visible)),.u-hidden-visually { /* same technique as the previous section */} That’s all the major behavior accounted for. Everything else in the demo is presentational.

    A Starting PointI wouldn’t consider this design shippable before exploring improvements that likely require JavaScript (for now, anyway). Things like:

    • A dynamic selection count (in case any have scrolled out of view)
    • A “Select all” function
    • Sorting
    • Filtering
    • The ability to shift-click to select spans of items
    • Deferring checkboxes till we’ve entered a selection mode
    • …and probably more!

    But as a foundation for progressive enhancement or, in my case, an in-browser mockup, it’s really exciting (and fun) how far HTML and CSS alone can take us.


    We’re Cloud FourWe solve complex responsive web design and development challenges for ecommerce, healthcare, fashion, B2B, SaaS, and nonprofit organizations.

    See our work