Build a CSS date range selector with :nth-child(of)

For years, a client asking for a custom booking calendar meant reaching for a heavy JavaScript library. It was the safe bet. It was also a maintenance problem, because managing the visual state of a CSS Date Range Selector in JavaScript means toggling classes, recalculating indices, and hoping nothing races the DOM update.

Modern CSS handles this now. The :nth-child(n of selector) syntax hands range styling to the browser’s engine. You write less code, and the styling keeps working when a user clicks too fast or a script fails to load.

How the “of” syntax works

The difference between .item:nth-child(2) and :nth-child(2 of .item) trips up most people. The standard selector finds the second child and then checks whether it carries the class, so if it does not, nothing matches. The “of” syntax filters the children by class first, then picks the second one out of that filtered list.

/* This fails if the 2nd child isn't an .accent */
.accent:nth-child(2) {
  font-weight: bold;
}

/* This successfully finds the 2nd element with .accent regardless of its position */
:nth-child(2 of .accent) {
  text-decoration: underline;
}

For more of these newer selectors, my guide on styling the ::search-text pseudo-element covers the accessibility side.

Building the calendar grid

Refactoring a legacy project onto CSS Grid showed me a few ways a calendar can break that I had not met before. The markup is a plain unordered list, and about three lines of CSS turn it into a responsive month layout. Each date is an <li> with a hidden checkbox inside it to carry the checked state.

<ul id="calendar">
  <li class="date">01<input type="checkbox" value="01"></li>
  <li class="date">02<input type="checkbox" value="02"></li>
  <!-- and so on -->
</ul>
#calendar {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  list-style: none;
}

Styling the CSS date range selector

JavaScript still manages the checkbox states when a user picks two dates, but the CSS Date Range Selector styling comes from combining :nth-child(of) with the sibling combinator (~). The goal is to style everything sitting between the first checked item and the second.

/* When two dates are selected */
.isRangeSelected { 
  /* Select dates following the first checked... */
  :nth-child(1 of :has(:checked)) ~ :not(:nth-child(2 of :has(:checked)) ~ .date) {
    background-color: rgb(228 239 253); 
    border-radius: 0;
  }
}

Read that compound selector out loud and it says: find everything after the first checked box, and stop once you reach the elements following the second checked box. The browser does the work, so there are no loops and no index math in JavaScript.

Managing state with vanilla JS

A small amount of logic still has to keep the selection at two dates. When someone clicks a third date, something has to decide whether that click becomes the new start or the new end. Per the MDN spec for :nth-child, you can target the first, second, or third checked box directly and uncheck the redundant one.

const CAL = document.getElementById('calendar');

CAL.addEventListener('change', e => {
  if (!CAL.querySelector(':checked')) return;
  
  // Toggle the range class if exactly two are checked
  CAL.classList.toggle('isRangeSelected', !!CAL.querySelector(':nth-child(2 of :has(:checked))'));

  // Logic for the third click
  if (CAL.querySelector(':nth-child(3 of :has(:checked))')) {
      // Uncheck the middle date or adjust as needed
      CAL.querySelector(':nth-child(2 of :has(:checked)) input').checked = false;
      CAL.classList.add('isRangeSelected');
  }
});

If front-end work like this is eating your dev hours, I can take it off your plate. I have been wrestling with WordPress since the 4.x days.

What this buys you in performance

Moving the range visualization into CSS cuts DOM mutations and script execution time. The UI feels quicker for it, most noticeably on mobile, where heavy JavaScript shows up as input lag. Support for :nth-child(of) is now broad across modern browsers, which leaves the old hacks with no real justification.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.