Building a Smarter CSS Date Range Selector with :nth-child(of)

For years, whenever a client asked for a custom booking calendar, I’d immediately reach for a bloated JavaScript library. It was the safe bet. But let’s be honest: managing the visual state of a CSS Date Range Selector in pure JavaScript is a maintenance nightmare. You’re constantly toggling classes, calculating indices, and praying you don’t hit a race condition when the DOM updates.

Modern CSS has finally caught up. With the :nth-child(n of selector) syntax, we can offload the heavy lifting of range styling to the browser’s engine. This isn’t just about writing less code; it’s about writing more resilient code that doesn’t break when a user clicks too fast or a script fails to load.

The “N of Selector” Logic

Most developers get tripped up by the difference between .item:nth-child(2) and :nth-child(2 of .item). Specifically, the standard selector looks for the second child and then checks if it has the class. If it doesn’t, nothing happens. In contrast, the “of” syntax filters all children for that class first, then picks the second one from 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;
}

If you’re looking for more ways to enhance your UI with modern selectors, check out my guide on styling the ::search-text pseudo-element for better accessibility.

Building the Calendar Grid

I honestly thought I’d seen every way a calendar could break until I refactored a legacy project using CSS Grid. By using a simple unordered list, we can create a fully responsive month layout in about three lines of CSS. Each date is an <li> containing a hidden checkbox to handle 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

Here is where the magic happens. When a user selects two dates, we use JavaScript to manage the checkbox states, but the CSS Date Range Selector styles are handled by combining :nth-child(of) with the sibling combinator (~). We want to style everything 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;
  }
}

This compound selector is a lifesaver. It essentially says: “Find everything after the first checked box, but stop once you hit the elements following the second checked box.” No loops, no complex math in JS, just browser-native performance.

Managing State with Vanilla JS

We still need a tiny bit of logic to ensure only two dates are selected at a time. If a user clicks a third date, we need to decide whether it becomes the new start or end date. Using the MDN spec for :nth-child, we can directly target the first, second, or third checked box in the DOM to 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');
  }
});

Look, if this CSS Date Range Selector stuff is eating up your dev hours, let me handle it. I’ve been wrestling with WordPress since the 4.x days.

The Performance Takeaway

By moving the range visualization to CSS, you reduce the number of DOM mutations and script execution time. It makes the UI feel snappier, especially on mobile devices where heavy JS can cause noticeable input lag. The :nth-child(of) syntax is now widely supported in modern browsers, so there’s little excuse for legacy hacks. Ship it.

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.

Leave a Comment