---
title: SC Time Slot Selector
description: Bookable time slots, converted to the visitor’s zone with luxon.
url: https://secretocho.design/lab/sc-time-slot-selector/
type: SoftwareSourceCode
status: working
published: 2026-10-05
---

# SC Time Slot Selector

> Bookable time slots, converted to the visitor’s zone with luxon.

component 28 / 28 · WORKING

- **type**: Code Component
- **platform**: Framer
- **license**: SecretoCho Design

![A still of the SC Time Slot Selector demo, as its module draws it with reduced motion.](https://secretocho.design/_astro/still.CDDJXVz0_ZT16T9.webp)

*fig 00 · live demo, ported from **Time\_Slot.tsx***

Time-slot picker for forms: slots from start to end at an interval, converted from the organizer’s zone to the visitor’s (luxon), 12/24 h toggle, 148 zones, hidden input.

SC Time Slot Selector lays out bookable time slots between a start and an end time at a chosen interval, generated in the organizer’s time zone and shown in the visitor’s. The visitor can switch the time zone from a list of 148 zones and toggle 12- and 24-hour display; the chosen slot goes into a hidden input for the Framer form, either as HH:mm or as a full ISO timestamp.

The companion of SC Calendar Widget in the old site’s booking flow; it uses luxon for the zone maths.

- Booking
- UI
- Internationalization

## Main

### Overview

- **Built With**: React (hooks), luxon (DateTime), Intl.DateTimeFormat, Framer Code Component API
- **Layout Support**: Fills the frame width; slots wrap
- **Orientation**: Wrapped slot chips, then a row with the zone selector and the 12hr \| 24hr toggle
- **Ideal Use Case**: Booking and consultation forms with clients in other time zones

### Tags

- time slots
- booking
- time zone
- luxon
- 12-hour
- 24-hour
- form
- hidden input

### Code comment

Time\_Slot.tsx has no doc comment: lines 202–207 hold only Framer’s annotations and the © line.

### Dependencies

- **Technologies**: luxon (third-party date library, imported as "luxon")
- **Notes**: The visitor’s zone comes from Intl.DateTimeFormat().resolvedOptions().timeZone; zones outside the file’s list fall back to UTC.

### Complexity

- **Logic Level**: Medium (slot generation across zones, dropdown with click-outside, two formats)
- **Runtime Cost**: Very Low (slots recomputed per render; one document mousedown listener)
- **Component Size**: ~17 KB TSX (17,049 bytes, 533 lines); 156 of the lines are the zone list
- **Notes**: luxon is the only external dependency among the drafts.

## Guide

### Instructions

1. Drop **SC Time Slot Selector** inside a Framer form.
2. Set **Start Time** and **End Time** (HH:mm), the **Interval** (15 min … 4 hr) and the **Origin Time Zone** (yours).
3. Choose **Default to 12-Hour**; the visitor can still toggle.
4. Set **Data Type** (time → HH:mm in the visitor’s zone; text → ISO timestamp with offset) and **Data Name**.
5. Style the accent, hover, background and text colours, the paddings, gap, font, sizes and border.

### Examples / Presets

- 30-minute slots
  
  - **interval**: 30
  
- 24-hour clock
  
  - **is12Hour**: false
  
- ISO values
  
  - **dataType**: text (the hidden input holds the full ISO timestamp with offset)
  

Source defaults: 09:00–18:00, 60 min, origin Europe/Belgrade, 12-hour. The demo also carries an action “As in the booking flow” built from the owner’s instance on the old site’s earlier home page.

## Mechanics

### How it Works

- **Slots**: generateTimeSlots parses start and end as HH:mm, builds luxon DateTimes for today in the origin zone, walks from start to end by the interval, re-zones each instant to the visitor’s zone and formats it “h:mm a” or “HH:mm”.
- **Value**: for Data Type time the slot value is HH:mm in the visitor’s zone; for text it is the ISO string with offset.
- **Zone**: the visitor’s zone is read once from Intl; if it is not in the file’s list of 148 zones, UTC is used. The zone pill opens a scrollable list; a document mousedown listener closes it on an outside click.
- **Toggle**: the 12hr | 24hr segmented control changes the display format only.
- **Form**: a hidden read-only \<input type=time|text name=dataName\> holds the selected value.

### Performance & Tuning

- Slots are recomputed on every render (no memo); with a 15-minute interval over a working day that is a few dozen DateTime objects — negligible.
- luxon adds a library to the page; it is the only dependency.

### Compatibility & Limits

- Evergreen browsers with Intl time zones; luxon handles the zone maths.
- The zone list is fixed in the file (148 zones); a visitor’s zone outside it shows as UTC until they pick another.
- The Font control is a Framer font object, but the file applies props.font directly as font-family: with a Font value selected the family may not apply (the default string “Inter, sans-serif” does).
- The zone box keeps a hard-coded #fff background (its text is inherited) and the inactive 12hr | 24hr segments #fff / #333, and the zone box draws its border whether or not border.show is on.
- The zone list is position: absolute at top 40 inside a wrapper that is not positioned, so it opens relative to the nearest positioned ancestor and can land over the slots.
- With Data Type time the form receives HH:mm in the visitor’s zone but not the zone itself; use text (ISO with offset) when the zone matters.
- Zone names must match the list exactly (it has Europe/Kiev, Asia/Kolkata, Asia/Kathmandu); a browser that reports another spelling (Asia/Calcutta, Asia/Katmandu, Europe/Kyiv) starts the visitor on UTC.
- A picked slot’s hidden value stays after a zone change while no slot shows as selected.
- Labels (“Time Zone:”, “12hr”, “24hr”) are English strings in the file.

## Quality

### Accessibility

- Slots are \<div role="button"\> without tabIndex or keyboard handlers; the zone list and toggle are clickable divs.
- Selected and hovered states are colour only; the hidden input is the only form control.

### Security & Privacy

- No network calls, no storage, no permissions.
- **Third-party library**: luxon (DateTime) for zone conversion and formatting.
- Reads the device time zone through Intl.DateTimeFormat (no permission prompt); one document mousedown listener while mounted.

## Reference

### Props

| name | type | default | description |
| --- | --- | --- | --- |
| startTime | string | 09:00 | Start Time (HH:mm, origin zone). |
| endTime | string | 18:00 | End Time (HH:mm, origin zone). |
| interval | enum | 60 | Interval. 15 min \| 30 min \| 1 hr \| 2 hr \| 4 hr (15, 30, 60, 120, 240 minutes). |
| originTimeZone | enum | Europe/Belgrade | Origin Time Zone. One of the file’s 148 IANA zones. |
| is12Hour | boolean | true | Default to 12-Hour. |
| accentColor | color | #3B82F6 | Accent Color (selected slot, active zone, active toggle). |
| hoverColor | color | #f0f0f0 | Hover Color. |
| backgroundColor | color | #fff | Background (slots and the zone list). |
| activeTextColor | color | #ffffff | Active Text (on the accent). |
| textColor | color | #4D4D4D | Text Color. |
| paddingSlot | string | 10px 8px | Slot Padding (CSS string). |
| paddingTimeZone | string | 6px 10px | TimeZone Padding (CSS string). |
| gridGap | string | 10px | Grid Gap between slots (CSS string). |
| font | FontStyle | Inter, sans-serif | Font (see the note under this table). |
| fontSize | number | 15 | Font Size Head 8–36 px (slots). |
| fontSizeBody | number | 13 | Font Size Body 8–36 px (zone row and toggle). |
| border | object | show=true, width=1, style=solid, color=#d2d2d2 | Border. Show, Width 0–4, Solid \| Dashed \| Dotted, Color. |
| dataType | enum | time | Data Type. time \| text for the hidden input’s type and value format. |
| dataName | string | Time | Data Name. The form field name. |

font is declared as ControlType.Font but used as a plain font-family string (style={{ fontFamily: props.font }}); only the default string works as intended. The interval control has no title in the file (Framer shows the prop name).

### Notes & Tips

- Set Origin Time Zone to where the meeting is held; the visitor sees their own times.
- Use Data Type text when the back end needs the date and offset, not just the clock time.
- Pair it with SC Calendar Widget: the slot’s ISO value carries today’s date, so the calendar supplies the day.

### Troubleshooting / FAQ

The zone shows UTC although my system zone is set

The browser’s name for the zone is not in the file’s list (it has Asia/Kolkata, Asia/Kathmandu and Europe/Kiev; some browsers report Asia/Calcutta, Asia/Katmandu or Europe/Kyiv); pick the nearest city from the dropdown.

Fewer slots than expected

End Time is inclusive only when it lands on the interval; 18:00 with 60-minute steps from 09:00 gives ten slots.

The font does not change

The Font control’s object is applied as font-family directly; use a plain family string or fix the mapping.

The zone box stays white on a dark theme

Its background (#fff) and the inactive segment colours (#fff / #333) are hard-coded; the slots and the zone dropdown list follow Background and Text Color.

The zone list opens over the slots

The list is absolutely positioned at top 40 and the component has no positioned wrapper; wrap it in a layer with position: relative or fix the file.

## Source

### Header comment

Time\_Slot.tsx has no doc comment: lines 202–207 hold only Framer’s annotations and the © line.

[props \[REF\]](#reference)

### Key excerpt

Slots are generated in the organizer’s zone and converted to the visitor’s: luxon walks from start to end by the interval, re-zones each instant and formats it 12- or 24-hour.

*Time\_Slot.tsx · lines 169–201 · tsx*

```tsx
function generateTimeSlots(
    start,
    end,
    interval,
    originZone,
    userZone,
    is12Hour,
    dataType
) {
    const slots = []
    let [startHour, startMinute] = start.split(":").map(Number)
    let [endHour, endMinute] = end.split(":").map(Number)

    const startDT = DateTime.fromObject(
        { hour: startHour, minute: startMinute },
        { zone: originZone }
    )
    const endDT = DateTime.fromObject(
        { hour: endHour, minute: endMinute },
        { zone: originZone }
    )

    for (let dt = startDT; dt <= endDT; dt = dt.plus({ minutes: interval })) {
        const local = dt.setZone(userZone)
        slots.push({
            display: formatTime(local, is12Hour),
            value:
                dataType === "time" ? formatTime(local, false) : local.toISO(),
        })
    }

    return slots
}
```

## Credits

### Credits

© SecretoCho Design 2025 — stated in the file (line 206).

- **license**: SecretoCho Design

SecretoCho Design 2025

### Disclaimer

Provided “as is” for visual/UX purposes. Validate performance, accessibility, and licensing within your project.

## Proves

- [Creative coding](https://secretocho.design/about/#capabilities)

## Hiring a design engineer?

I’m open to senior design engineer roles: remote from Belgrade, or relocation with visa sponsorship. Write to me, or read the CV first. Client work stays selective, one project at a time.

[sc@secretocho.design](mailto:sc@secretocho.design)

[read the CV](https://secretocho.design/cv/) · [LinkedIn](https://www.linkedin.com/in/secretocho/)
