GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 Date Range Picker UI Patterns — Copy-Paste

A date range picker UI is a control that lets someone choose a span, not just one value.

Auto-plays · click a tile to jump to its section · all nine in one zip

The nine date range selector ui patterns are ordered by how many values you're choosing at once, not by popularity. 01 and 02 pick one range (start–end, min–max); 03, 04, and 05 pick one option out of several as a card, an avatar, or a chip; 06 and 07 find one item by searching or scrolling; 08 picks a ratio; 09 moves several items at once between two lists. Narrowing this picker pillar down to a few pills gives 9 Segmented Control UI Patterns, and unfolding the list downward gives 9 Select Dropdown Design Patterns.

01Calendar drag range select

What this date range picker ui design shows

Tap a start day, then an end day, on the month grid — the days between fill with a soft tint while the two edges get solid dots.

Calendar drag range select
Open in a new tab

01 · Calendar drag range select — Measured area 18.911% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

방향키 이동aria-selected0.22초

Where to look in the code

It fits a stay-length picker on a booking page or a vacation date range field.

.cal__day.is-range { background: rgba($color, .18); }
.cal__day.is-edge {
  background: $color;
  color: #fff;
  font-weight: 700;
  transform: scale(1.06);
}

02Dual-thumb price range slider

What happens on screen

Drag either handle — or use arrow keys — and the min/max labels update live while only the span between them fills with color.

Dual-thumb price range slider
Open in a new tab

02 · Dual-thumb price range slider — Measured area 3.265% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointer 드래그role=slider방향키 ±1

How to read the code

It suits a price-range filter on a shop page or a size-range filter for listings.

function pctFromX(x) {
  var r = track.getBoundingClientRect();
  return Math.round(Math.max(0, Math.min(100, ((x - r.left) / r.width) * 100)));
}
function setLo(n) { v.lo = Math.max(0, Math.min(n, v.hi - 5)); paint(); }
function setHi(n) { v.hi = Math.min(100, Math.max(n, v.lo + 5)); paint(); }

03Card-style plan radio select

What moves

With :has(), only the picked card lifts with a bright border while a check badge scales into view in its corner.

Card-style plan radio select
Open in a new tab

03 · Card-style plan radio select — Measured area 3.631% · moving frames 6/23Original by GODRICH · SCSS and React ship in the zip · See the password

:has()scale 배지0.22초

What to change

It works for pricing-plan cards or shipping-method cards.

.plan:has(.plan__radio:checked) {
  transform: translateY(-3px);
  border-color: $color;
  box-shadow: $shadow-raised;
}
.plan:has(.plan__radio:checked) .plan__badge { transform: scale(1); opacity: 1; }

04Avatar icon grid picker

What you see

Picking an avatar in the grid grows a ring in that avatar's own accent color and swaps the name in the preview above.

Avatar icon grid picker
Open in a new tab

04 · Avatar icon grid picker — Measured area 5.119% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

role=radiogroup방향키 순환색상별 링

Where to look in the code

It fits profile avatar selection or a character-select screen.

function select(idx) {
  cur = idx;
  items.forEach(function (b, i) {
    b.setAttribute('aria-checked', i === idx ? 'true' : 'false');
    b.tabIndex = i === idx ? 0 : -1;
    b.classList.toggle('is-on', i === idx);
  });
  pvEmoji.textContent = items[idx].dataset.emoji;
  pvName.textContent = items[idx].dataset.name;
}

05Size chip + quantity stepper combo

What happens on screen

Picking a size chip changes the unit price, and the +/- stepper recalculates the subtotal on the spot.

Size chip + quantity stepper combo
Open in a new tab

05 · Size chip + quantity stepper combo — Measured area 3.278% · moving frames 8/23Original by GODRICH · SCSS and React ship in the zip · See the password

aria-live 수량min/max 비활성화0.22초

How to read the code

It suits a product page where size and quantity are chosen together before adding an item to the cart.

function price() {
  return +(radios.filter(function (r) { return r.checked; })[0] || radios[0]).dataset.price;
}
function paint() {
  num.textContent = qty;
  dec.disabled = qty <= 1; inc.disabled = qty >= 9;
  total.textContent = (price() * qty).toLocaleString('en-US') + ' KRW';
}

06Search combobox listbox

What moves

Typing filters the list to options starting with those letters, and arrow-key movement updates aria-selected on the active option while focus stays on the input.

Search combobox listbox
Open in a new tab

06 · Search combobox listbox — Measured area 7.848% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password

role=comboboxaria-selected0.18초

What to change

It fits a country or city search-select field, or a recipient picker after a search.

function render(q) {
  shown = all.filter(function (c) { return c.indexOf(q) === 0 || !q; }).slice(0, 3);
  list.innerHTML = '';
  shown.forEach(function (c, i) {
    var li = document.createElement('li');
    li.setAttribute('role', 'option');
    li.setAttribute('aria-selected', i === active ? 'true' : 'false');
    li.textContent = c;
    list.appendChild(li);
  });
}

07Scroll-snap number wheel picker

What you see

Scrolling vertically enlarges and darkens whichever number lands in the center slot, and scroll-snap locks it exactly in place on release.

Scroll-snap number wheel picker
Open in a new tab

07 · Scroll-snap number wheel picker — Measured area 0.316% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

scroll-snap-type방향키 스크롤role=listbox

Where to look in the code

It's built for an alarm-hour picker or a party-size wheel on a booking form.

function mark() {
  var idx = Math.round(track.scrollTop / H);
  idx = Math.max(0, Math.min(N - 1, idx));
  if (idx === centered) return;
  centered = idx;
  items.forEach(function (el, i) { el.classList.toggle('is-center', i === idx); });
  track.setAttribute('aria-activedescendant', items[idx].id);
}

08Aspect-ratio crop preset picker

What happens on screen

Picking a 1:1, 4:5, 16:9, or 9:16 chip smoothly morphs the preview box's aspect-ratio to match.

Aspect-ratio crop preset picker
Open in a new tab

08 · Aspect-ratio crop preset picker — Measured area 12.704% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

aspect-ratio 전환role=radiogroup0.3초

How to read the code

It fits cropping a profile photo or setting an upload ratio for Reels or Shorts.

function apply(v) { box.style.aspectRatio = v; }
radios.forEach(function (r) {
  r.addEventListener('change', function () { apply(r.value); });
});
.ar__box {
  aspect-ratio: 1 / 1;
  height: 100%;
  border: 2px solid $color;
  transition: aspect-ratio $duration $easing;
}
.ar__radio:checked ~ span { background: $color; color: #fff; }

09Dual-list transfer picker

What moves

Select items in the left list and press the arrow button to move them to the right list; arrow keys roam within each list.

Dual-list transfer picker
Open in a new tab

09 · Dual-list transfer picker — Measured area 1.42% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

role=listbox다중 선택aria-multiselectable

What to change

It's built for assigning permissions or roles, or building an email recipient group.

function move(from, to, sel) {
  var keep = [], moved = [];
  from.forEach(function (n) { (sel[n] ? moved : keep).push(n); });
  from.length = 0; Array.prototype.push.apply(from, keep);
  Array.prototype.push.apply(to, moved);
  Object.keys(sel).forEach(function (k) { delete sel[k]; });
  render();
}

Where it breaks — a hidden decorative layer still counts toward scroll size

Building 01, I first added a purely decorative sweep bar behind the calendar — a ::before that only used translateX and was meant to loop on its own, separate from the real click logic. Once it moved translateX(366%), the bar sat visually off the stage, but document.documentElement.scrollWidth still grew by that distance, so both the 480×300 render and the 320px phone check reported overflow. opacity: 0 and pointer-events: none hide a layer from view, but its box and travel distance are still measured. The fix was to drop the decorative layer and drive the loop with the real .is-range/.is-edge state changes instead. 06's search list hit a related trap: a closed list panel that's only hidden with opacity keeps its full height in the box tree, so the initial render still overflows — only max-height: 0; overflow: hidden; actually collapses a closed panel. 03's :has() only runs on Chrome/Edge 105+, Safari 15.4+, Firefox 121+; below that, the card just doesn't lift, but the underlying radio selection still works. The avatar pieces that show who got picked are gathered in Avatar UI Design. The zip password for all nine files is uv8qjazc.

Accessibility

03, 05, and 08 hide a real <input type="radio"> visually only, so a screen reader still reads it as a radio and arrow keys still move between options, while 01 uses role="gridcell" day buttons with aria-selected and its own arrow-key grid navigation instead of a native input. 02, 04, 07, and 09 are custom controls, so each carries role="slider", role="radiogroup", or role="listbox" along with aria-valuenow, aria-checked, or aria-selected, updated on every state change. 06 announces the highlighted option through aria-selected on the active role="option" item without moving focus from the input, and 05's quantity is aria-live="polite" so a new count is read quietly after every tap. Under prefers-reduced-motion: reduce, all nine drop their transitions, and the selected state — a check, a ring, a badge — stays in its final shape instead of disappearing.

@media (prefers-reduced-motion: reduce) {
  .cal__day, .plan, .ava__item, .ar__box { transition: none; }
}

For more single components in the picker family, see 9 Segmented Control UI Patterns; the select-and-dropdown half of the same family is in 9 Select Dropdown Design Patterns.

FAQ

How is this date range picker html different from two separate date fields?

Two date fields take a start and an end value independently, but 01 lets you tap both on one grid so the days between fill in immediately — you see the length of the stay, not just two dates. That matters most on booking pages where the span itself is the thing being decided. Choosing a clock time and a duration rather than a date is handled by 9 time picker ui patterns.

What happens if the two thumbs on the price slider get pushed together?

02's setLo and setHi always keep the minimum at least 5 units below the maximum. Push one handle toward the other and the other handle gets nudged along by that minimum gap, so the two thumbs never overlap or swap order.

Does the scroll-snap wheel picker work without a mouse wheel?

Yes — 07's track has scroll-snap-type: y mandatory, so a touch swipe settles naturally, and focusing the track and pressing an arrow key calls scrollBy({ top, behavior: 'smooth' }) to move one row at a time. The mouse wheel is just one more way to trigger the same scroll.

Enter the archive password

The password is inside this article. You will find it as you read.