Mischief

129 / Controls

Weekly Hours

Weekly availability, the settings screen of every booking tool. Each day is on or off and holds one or more time ranges, and mistakes are flagged where they are made.

Office hours

When students can book a call with you.

  • to
  • to
  • to
  • to
  • to
  • to
Unavailable
  • to
Unavailable
  • to

Ready to save.

Installation

Copy the source into your project, or keep it behind a package.

npx shadcn@latest add Tinkerers-Labs/mischief-ui/weekly-hours
import { WeeklyHours } from "mischief-ui/weekly-hours"
Also installs
  • lucide-react

Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.

registry/default/weekly-hours/weekly-hours.tsx
"use client" import * as React from "react"import { Copy, Plus, X } from "lucide-react" import { cn } from "@/lib/utils" export type Weekday =  | "sunday"  | "monday"  | "tuesday"  | "wednesday"  | "thursday"  | "friday"

Usage

export function Availability() {
  const [week, setWeek] = React.useState<WeeklyHoursValue>(savedWeek)
  const blocked = getWeeklyHoursIssues(week).length > 0

  return (
    <form onSubmit={() => save(week)}>
      <WeeklyHours value={week} onValueChange={setWeek} />
      <button disabled={blocked}>Save</button>
    </form>
  )
}

The value

One list of ranges per weekday, keyed by lowercase English day name. Times are 24 hour HH:mm strings whatever clock is shown, and an end of 24:00 is midnight. An empty list means the day is unavailable, so there is no separate flag to keep in step.

const week: WeeklyHoursValue = {
  monday: [{ start: "09:00", end: "17:00" }],
  tuesday: [
    { start: "09:00", end: "12:00" },
    { start: "13:00", end: "17:00" },
  ],
  wednesday: [], thursday: [], friday: [], saturday: [], sunday: [],
}

Switching, adding and copying

Switching a day off folds its ranges away and remembers them, so switching it back on restores them. A day switched on for the first time gets defaultRange, nine to five unless you say otherwise. Add puts a one hour range an hour after the day's last one. Removing the last range switches the day off. Copy sets every day of the week to that day's ranges, including days that were off.

Errors where they happen

A range that ends at or before its start, and ranges on one day that run into each other, are marked invalid with a line under the range naming the problem. Nothing is blocked or rearranged while someone is still choosing. getWeeklyHoursIssues returns the same findings as data, so a form can refuse to save a week the editor is flagging.

Language, clock and first day

Day names and times come from Intl in locale, or the reader's browser language when it is left out. The clock follows the locale unless hourCycle says h12 or h23, and the week starts on the locale's first day where the browser reports it, Monday otherwise. Pass firstDay to fix it. Server rendering uses en-US until the page hydrates, so pass locale if the server should already render the reader's language.

API

valueWeeklyHoursValueThe week, for a controlled editor.
defaultValueWeeklyHoursValueThe starting week. Defaults to nine to five, Monday to Friday.
onValueChange(value: WeeklyHoursValue) => voidCalled with the whole week after every change.
step15 | 30Minutes between the times on offer. Defaults to 30.
localestringLanguage for day names and times.
hourCycle"h12" | "h23"Forces a 12 or 24 hour clock.
firstDayWeekdayThe day the list starts on.
defaultRangeTimeRangeWhat a day gets when switched on for the first time.
labelstringThe accessible name of the editor. Defaults to Weekly hours.
disabledbooleanMakes every control unavailable.
classNamestringClasses for placement and appearance.
...divPropsHTMLAttributesNative div attributes.
value (getWeeklyHoursIssues)Partial<WeeklyHoursValue>Returns { day, index, kind, other? } for every range that ends before it starts or overlaps another.

Accessibility

The editor is a named group. Each day's switch is a native button with the switch role, named by the day, so it reads as Monday, on. Times are native selects, which open the platform's own picker on phones, named with the day and, when a day has several ranges, their number. Add, copy and remove buttons name the day and the range they act on. An invalid range sets aria-invalid on both selects and links the message as their description, and a polite live region repeats it, along with the result of a copy. Focus moves to a new range's start time, to the neighbouring range after a removal, and to the day's switch when its last range goes. Folded ranges are inert. Reduced motion removes the fold and the switch slide.