Maintenance Window

A maintenance banner in the reader's own time zone, with a countdown and a warning before starting a job that would be cut off.

StopsWork lost to planned downtime

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/maintenance-window.json

The files land in your project. They're yours to edit.

Usage

maintenance-window-usage.tsx
import { MaintenanceTaskButton, MaintenanceWindow } from "@/components/ui/maintenance-window"

<MaintenanceWindow start={start} end={start + 30 * 60_000} hasUnsavedChanges={draft.length > 0}>
  <div className="grid gap-3 rounded-lg border bg-background p-3">
    <label className="grid gap-1.5">
      <span className="text-sm font-medium">Notes</span>
      <textarea
        rows={2}
        value={draft}
        onChange={(event) => setDraft(event.target.value)}
        className="w-full resize-none rounded-md border bg-transparent px-3 py-2 text-base shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed md:text-sm dark:bg-input/30"
      />
    </label>
    <MaintenanceTaskButton estimateMinutes={45} onStart={() => setStarted(true)} anywayLabel="Export anyway">
      Export all invoices
    </MaintenanceTaskButton>
    {started && <p className="text-xs text-muted-foreground">Export started.</p>}
  </div>
</MaintenanceWindow>

What it catches

  • Downtime shown in the wrong time zone: converted to the reader's
  • Starting a long job that maintenance would cut off
  • Reloading with unsaved changes during the window (beforeunload warning)
  • Window crossing midnight

Props

MaintenanceWindow props
PropTypeDefault
start* (required)

When maintenance begins.

When—
end* (required)

When it is expected to end.

When—
locale

Locale for dates; defaults to the browser's.

string—
timeZone

Time zone for display; defaults to the reader's own.

string—
soonMinutes

How long before the start the banner turns into a countdown, in minutes.

number60
hasUnsavedChanges

True while the wrapped form holds unsaved changes; during the window a reload is then warned about.

booleanfalse
scheduledText

Templates: `{window}`, `{countdown}`, `{start}`, `{end}` are filled in.

string"Scheduled maintenance {window} · in {countdown}"
soonText

Template shown when the window is close.

string"Maintenance starts in {countdown}, at {start}. Editing p…
activeText

Template shown during the window.

string"Maintenance until {end} · {countdown} left. Editing is p…
overText

Shown after the window ends.

string"Maintenance is over. Your changes are still here; save w…
dismissLabel

Accessible label for the dismiss button.

string"Got it"

* required. Also accepts the native props of <div> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Banner is role=status
  • Cut-off hint is a polite live region linked to the start button
  • Dismiss is a button; icons are aria-hidden

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: Reject writes during maintenance on the server with a clear error so clients can retry later.

  • Disables only the fields it wraps (a disabled fieldset) in this tab; it doesn't make your app or API read-only, so block writes on the server during the window
  • Times come from your start and end props
  • The reload warning depends on the browser showing beforeunload prompts

What it stops

Work lost to planned downtime

Also known as

maintenance · downtime · scheduled maintenance · banner · countdown · read only · status page · outage

Field type

Dependencies

Pairs well with