Trip search
Where, when and who for a trip.
45 minutes with Rui · times in Porto (WET)
No free times
Tue 18 Nov
No free times
Wed 19 Nov
No free times
Thu 20 Nov
No free times
Last time left that day
Live preview: try it.
<style>
.odl-time-slots { box-sizing: border-box; inline-size: 100%; max-inline-size: 40rem; margin-inline: auto; container-type: inline-size; display: grid; gap: var(--ds-space-stack-lg); padding: var(--ds-space-inset-xl);
background: var(--ds-color-surface-raised); color: var(--ds-color-text-primary); border: var(--ds-border-width-default) solid var(--ds-color-border-subtle); border-radius: var(--ds-radius-container);
box-shadow: var(--ds-elevation-raised); font: var(--ds-text-body-md); text-wrap: pretty; }
.odl-time-slots *, .odl-time-slots *::before { box-sizing: border-box; }
.odl-time-slots header { display: grid; gap: var(--ds-space-stack-xs); }
.odl-time-slots h3 { margin: 0; font: var(--ds-text-title-md); overflow-wrap: anywhere; }
.odl-time-slots .odl-tz { margin: 0; color: var(--ds-color-text-secondary); font: var(--ds-text-label-md); overflow-wrap: anywhere; }
.odl-time-slots .odl-days { display: grid; }
.odl-time-slots .odl-day { display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); align-items: start; gap: var(--ds-space-stack-sm) var(--ds-space-inline-md); padding-block: var(--ds-space-stack-md); }
.odl-time-slots .odl-day:first-child { padding-block-start: 0; }
.odl-time-slots .odl-day:last-child { padding-block-end: 0; }
.odl-time-slots .odl-day + .odl-day { border-block-start: var(--ds-border-width-default) solid var(--ds-color-border-separator); }
.odl-time-slots .odl-dlabel { margin: 0; min-block-size: max(var(--ds-size-control-md), var(--ds-size-target-min)); display: flex; align-items: center; font: var(--ds-text-label-lg); font-weight: var(--ds-font-weight-label); overflow-wrap: anywhere; }
.odl-time-slots ul { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr)); gap: var(--ds-space-target-gap) var(--ds-space-inline-sm); }
.odl-time-slots li { display: grid; min-inline-size: 0; }
.odl-time-slots .odl-wrap { display: grid; min-inline-size: 0; }
.odl-time-slots .odl-slot { position: relative; display: inline-flex; align-items: center; justify-content: center; min-inline-size: 0; min-block-size: max(var(--ds-size-control-md), var(--ds-size-target-min)); padding: var(--ds-space-inset-xs) var(--ds-space-inset-sm);
border: var(--ds-border-width-default) solid var(--ds-color-border-default); border-radius: var(--ds-radius-control); background: var(--ds-color-surface-raised); color: var(--ds-color-text-primary);
font: var(--ds-text-label-lg); font-weight: var(--ds-font-weight-label); font-variant-numeric: tabular-nums; text-align: center; overflow-wrap: anywhere; cursor: pointer;
transition: background-color var(--ds-motion-duration-short) var(--ds-motion-easing-standard), border-color var(--ds-motion-duration-short) var(--ds-motion-easing-standard); }
.odl-time-slots .odl-slot:hover { background: var(--ds-color-bg-neutral-subtle); border-color: var(--ds-color-border-strong); }
/* almost full: a small dot in the corner; say so in words too (the action's a11y_label) */
.odl-time-slots [data-tone="neutral"] .odl-slot::after { content: none; }
.odl-time-slots [data-tone="warning"] .odl-slot::after { content: ""; position: absolute; inset-block-start: var(--ds-space-inset-xs); inset-inline-end: var(--ds-space-inset-xs);
inline-size: var(--ds-space-6); block-size: var(--ds-space-6); border-radius: var(--ds-radius-full); background: var(--ds-color-bg-warning-bold); }
.odl-time-slots .odl-slot[aria-pressed="true"] { border-color: transparent; background: var(--ds-color-bg-action-primary); color: var(--ds-color-text-on-action); }
.odl-time-slots .odl-slot:disabled { cursor: not-allowed; border-color: var(--ds-color-border-subtle); background: var(--ds-color-bg-neutral-subtle); color: var(--ds-color-text-secondary); text-decoration: line-through; }
.odl-time-slots .odl-slot:focus-visible { outline: var(--ds-focus-ring-width) solid var(--ds-color-border-focus); outline-offset: var(--ds-focus-ring-offset); }
/* a day with no times says so, in place of the buttons */
.odl-time-slots .odl-none { display: none; margin: 0; min-block-size: max(var(--ds-size-control-md), var(--ds-size-target-min)); align-items: center; color: var(--ds-color-text-secondary); font: var(--ds-text-body-md); }
.odl-time-slots .odl-day:not(:has(.odl-slot)) .odl-none { display: flex; }
.odl-time-slots .odl-day:not(:has(.odl-slot)) ul { display: none; }
.odl-time-slots .odl-legend { display: flex; align-items: center; gap: var(--ds-space-inline-sm); margin: 0; color: var(--ds-color-text-secondary); font: var(--ds-text-label-md); overflow-wrap: anywhere; }
.odl-time-slots .odl-legend::before { content: ""; flex: none; inline-size: var(--ds-space-6); block-size: var(--ds-space-6); border-radius: var(--ds-radius-full); background: var(--ds-color-bg-warning-bold); }
/* Phone: the day's name sits above its times. */
@container (max-width: 30rem) {
.odl-time-slots .odl-day { grid-template-columns: minmax(0, 1fr); }
.odl-time-slots .odl-dlabel { min-block-size: 0; }
.odl-time-slots ul { grid-template-columns: repeat(auto-fill, minmax(4.75rem, 1fr)); }
}
@media (max-width: 30rem) { .odl-time-slots { padding: var(--ds-space-inset-lg); } }
/* the hidden template item (data-od-template) stays out of sight in the static file */
.odl-time-slots [hidden] { display: none !important; }
@media (prefers-reduced-motion: reduce) { .odl-time-slots, .odl-time-slots *, .odl-time-slots *::before, .odl-time-slots *::after { transition: none !important; animation: none !important; } }
</style>
<section class="odl-time-slots" aria-label="Pick a time for your haircut" data-od-label="title">
<header><h3 data-od-slot="title">Pick a time for your haircut</h3><p class="odl-tz" data-od-slot="timezone" data-od-optional>45 minutes with Rui · times in Porto (WET)</p></header>
<div class="odl-days" data-od-list="days">
<div class="odl-day" role="group" data-od-label="label" data-od-item data-od-template hidden><p class="odl-dlabel" data-od-slot="label"></p>
<ul data-od-list="slots"><li data-od-item><span class="odl-wrap" data-od-var="tone" data-tone="neutral"><button type="button" class="odl-slot" data-od-action="select"></button></span></li></ul>
<p class="odl-none">No free times</p></div>
<div class="odl-day" role="group" aria-label="Tue 18 Nov" data-od-item><p class="odl-dlabel">Tue 18 Nov</p>
<ul><li><span class="odl-wrap" data-tone="neutral"><button type="button" class="odl-slot" aria-label="09:30, Tue 18 Nov">09:30</button></span></li>
<li><span class="odl-wrap" data-tone="neutral"><button type="button" class="odl-slot" aria-label="11:00, Tue 18 Nov" aria-pressed="true">11:00</button></span></li>
<li><span class="odl-wrap" data-tone="neutral"><button type="button" class="odl-slot" aria-label="14:15, Tue 18 Nov">14:15</button></span></li>
<li><span class="odl-wrap" data-tone="warning"><button type="button" class="odl-slot" aria-label="17:45, Tue 18 Nov, last one">17:45</button></span></li></ul>
<p class="odl-none">No free times</p></div>
<div class="odl-day" role="group" aria-label="Wed 19 Nov" data-od-item><p class="odl-dlabel">Wed 19 Nov</p>
<ul></ul><p class="odl-none">No free times</p></div>
<div class="odl-day" role="group" aria-label="Thu 20 Nov" data-od-item><p class="odl-dlabel">Thu 20 Nov</p>
<ul><li><span class="odl-wrap" data-tone="neutral"><button type="button" class="odl-slot" aria-label="10:00, Thu 20 Nov">10:00</button></span></li>
<li><span class="odl-wrap" data-tone="warning"><button type="button" class="odl-slot" aria-label="12:30, Thu 20 Nov, last one">12:30</button></span></li>
<li><span class="odl-wrap" data-tone="neutral"><button type="button" class="odl-slot" aria-label="15:00, Thu 20 Nov">15:00</button></span></li></ul>
<p class="odl-none">No free times</p></div>
</div>
<p class="odl-legend" data-od-slot="legend" data-od-optional>Last time left that day</p>
</section>
<script>
document.querySelectorAll(".odl-time-slots").forEach((root) => {
if (root.dataset.odlReady) return; // wire once
root.dataset.odlReady = "1";
// the picked time stays marked until another is picked; the host gets the choice from the button itself
const slots = [...root.querySelectorAll(".odl-slot")];
slots.forEach((b) => b.addEventListener("click", () => {
slots.forEach((x) => x.setAttribute("aria-pressed", String(x === b)));
}));
});
</script>
Where, when and who for a trip.
Pick a start and end day from two months side by side.
Search, filters and sort above a table.