webdevpuneet.com

Availability Scheduler — Weekly Time Grid HTML CSS

Availability Scheduler · Forms · Plain HTML, CSS & JS · Live preview

CategoryForms

Copy & paste

Source Code

<div class="avs-card">
  <div class="avs-head">
    <div>
      <h3>Set your weekly availability</h3>
      <p>Click or drag across the grid to mark when you're free.</p>
    </div>
    <button type="button" class="avs-clear" id="avsClear">Clear all</button>
  </div>

  <div class="avs-grid-wrap">
    <div class="avs-grid" id="avsGrid"></div>
  </div>

  <div class="avs-foot">
    <span class="avs-legend"><i class="avs-swatch on"></i> Available <i class="avs-swatch off"></i> Unavailable</span>
    <span class="avs-total"><b id="avsHours">0</b> hrs / week</span>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Click-and-drag paint selection
Drag across cells to mark a whole time block in one gesture, with fill-or-clear decided by the first cell touched.
Data-driven grid
Days, start hour, and slot count generate the entire grid and labels — change them for any schedule or granularity.
Live weekly hours total
The footer counter equals Object.keys(selected).length, updating on every change with no separate tally to maintain.
Selection doubles as submit data
The selected set's keys are the chosen day-slots, trivial to serialize and send to a backend.
Synced data and visuals
setCell() updates the data object and the cell class together, so the count and grid never drift apart.
Clear-all reset
One button empties the selection and the grid in a single pass.
Clear two-state legend
A legend and solid indigo selected cells make available vs unavailable obvious at a glance.
Horizontal scroll on mobile
The grid scrolls inside its wrapper so it stays usable on narrow screens without layout breakage.

About this UI Snippet

Availability Scheduler — Drag-to-Paint Weekly Time-Slot Grid

Screenshot of the Availability Scheduler snippet rendered live

Asking someone "when are you free?" with a text box gets you vague, error-prone answers. A visual availability grid — days across one axis, hours across the other, paint the cells you're free — gets you precise, structured data in seconds. This snippet builds that weekly scheduler in plain HTML, CSS, and vanilla JavaScript, with the click-and-drag painting interaction that makes selecting a whole block of time effortless instead of a dozen individual clicks.

Drag-to-paint, the interaction that makes it fast

The core of a good availability picker is being able to drag across a range of cells rather than click each one. On mousedown, the grid records whether you're painting cells *on* or *off* based on the first cell you touch (paintValue = !selected[key]), so dragging from an empty cell fills, and dragging from a filled cell clears. As the cursor moves over more cells (mouseover while painting is true), each one gets set to that same value — so a single drag from Monday-9am to Monday-5pm marks the whole workday available in one gesture. A document-level mouseup ends the paint, so releasing anywhere stops it cleanly.

A grid built from data

The whole grid is generated from three values: a DAYS array, a START_HOUR, and a SLOTS count (9am–6pm, one cell per hour by default). build() lays it out as a CSS grid with a day-label column plus one hour-header row, and each cell carries a data-key of "day-slot". Change the days, the start hour, or the number of slots and the entire grid — labels included — regenerates. Want 30-minute granularity? Double SLOTS and adjust the label function.

Selection stored as a simple set

Selected cells live in a plain selected object keyed by "day-slot", which doubles as the data you'd submit: its keys *are* the chosen slots, and Object.keys(selected).length is the live hours total shown in the footer. There's no parallel array to keep in sync with the DOM — setCell() updates both the data and the cell's class together, so the count and the visuals never drift. Serializing this for a backend is trivial (covered in the FAQs).

Honest, scannable feedback

A legend explains the two states, and the running "hrs / week" total updates on every change so the user always knows how much availability they've committed to. The clear-all button resets both the data and the grid in one pass. Hover scales each cell slightly so the paint target is obvious, and selected cells use a solid indigo that reads clearly against the empty light-gray slots.

Touch and accessibility notes

The drag interaction is pointer-based, so it's most natural on desktop; on touch, tapping individual cells still works, and the FAQs cover adding full touch-drag with touchmove and elementFromPoint. For keyboard and screen-reader support you'd make each cell a real toggle button with aria-pressed — the data model doesn't change, only the input layer. The grid scrolls horizontally inside its wrapper so it stays usable on narrow screens without breaking the layout.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA weekly grid renders with days as rows and hours (9a–6p) as columns, all cells empty.
  2. 2
    Click a cellClick any cell to mark that day-and-hour available — it fills indigo and the weekly hours total updates.
  3. 3
    Drag to paint a blockPress and drag across cells to mark a whole range at once — dragging from an empty cell fills, from a filled cell clears.
  4. 4
    Watch the totalThe "hrs / week" counter in the footer reflects exactly how many slots are selected at all times.
  5. 5
    Clear and restartClick "Clear all" to reset the entire grid and the total in one action.
  6. 6
    Customize the gridEdit DAYS, START_HOUR, and SLOTS to change the days, time range, or granularity — the grid and labels regenerate.

Real-world uses

Common Use Cases

Booking and consultation availability
Let providers set the hours they take appointments — pair with a time slot picker for clients to book within them.
Meeting and interview scheduling
Collect everyone's free blocks to find overlap, pairing with a timezone converter for distributed teams.
Shift and staff rostering
Have employees paint their available shifts for the week as structured input to a scheduling system.
Tutor and coaching platforms
Set recurring weekly availability that students book against.
Volunteer and event sign-ups
Let participants indicate which time blocks they can cover across a week.
Learning drag-selection patterns
A reference for paint-on-drag grids and set-based selection — compare with an activity heatmap for a read-only grid.
Related: Notion-Style Block Editor
See the Notion-Style Block Editor for a related forms pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

The selected object's keys are already your data — each "day-slot" key (e.g. "0-3" = Monday, 4th slot) represents one available block. Serialize Object.keys(selected) directly, or map them to start/end times using START_HOUR and the slot index, and POST that array. On load, populate selected from saved data and call setCell() for each to restore the grid.

Increase SLOTS (double it for 30-minute granularity, quadruple for 15) and update the hourLabel function to compute the time from the slot index accordingly. The grid, painting, and total all derive from SLOTS, so no other code changes — just the label math and the column count.

The mouse-based paint works for taps but not touch-drag. Add touchstart/touchmove listeners that read e.touches[0].clientX/Y, call document.elementFromPoint() to find the cell under the finger, and apply setCell() with the paint value — mirroring the mouseover logic. Add touch-action: none to the grid so the page doesn't scroll mid-paint.

Render each cell as a <button> with aria-pressed reflecting its on/off state and a descriptive aria-label ("Monday 9am"), so it's focusable and toggleable with Enter/Space and announced correctly. The selected data model stays identical; only the cell element and its event wiring change.

In React, hold the selected set in useState (a Set or object) and toggle on cell events, deriving the total from its size; in Vue, use reactive() with a computed total; in Angular, use a component Set field with a getter. The paint-on-drag logic uses a "painting" flag and a captured paintValue that port directly to each framework's pointer events.