Focus session
StableA Pomodoro session: choose a task, say what you’ll finish, then focus and break in phases, with overtime and an inline Abandon.
FocusSession works in phases: Focus, a short break, Focus again, and a long break after four (all adjustable in its settings). Past zero a phase keeps counting as overtime until you finish it, which logs the time with Undo; Abandon asks inline first and logs nothing. It reads the session from the nearest FocusProvider, and brings its own (saved as “timer”) when there is none.
Updated View source
A focus session
import { SampleBadge } from '@rchursin/product/shell/layout'import { FocusProvider, FocusSession } from '@rchursin/product/timer'
/** * Pick a task, say what you will get done, press Start. Click the background of the session first * and Space starts and pauses, F finishes and logs. This example keeps its own session * (storagePrefix), so it never shares a run with the Timer app. */export default function FocusSessionExample() { return ( <FocusProvider storagePrefix="library:focus-session"> {/* isolate + overflow-hidden keep the soft halo behind the clock inside the frame */} <div className="relative isolate flex w-full flex-col items-center gap-6 overflow-hidden px-4 py-8 sm:px-8 sm:py-10"> <SampleBadge /> <FocusSession /> </div> </FocusProvider> )}Usage
import { FocusProvider, FocusSession } from '@rchursin/product/timer'<FocusProvider storagePrefix="focus"> <FocusSession /></FocusProvider>API
FocusSession props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | – | Classes on the root, which is also the keyboard scope (tabIndex −1). |
TaskPicker props
| Prop | Type | Default | Description |
|---|---|---|---|
compact | boolean | false | 32px tall instead of 36, for the widget. |
className | string | – | Classes on the button. |
PhaseChip props
| Prop | Type | Default | Description |
|---|---|---|---|
phaserequired | 'focus' | 'short' | 'long' | – | Focus is solid, a short break outlined, a long break dashed, each with its name and icon. |
size | 'xs' | 'sm' | 'sm' | Chip height. |
className | string | – | Classes on the chip. |
OvertimeChip props
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'xs' | 'sm' | 'sm' | Chip height. It says “Overtime” next to the phase once the clock passes zero. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Space | Start or pause, while focus is on the session’s background (click it first). On a button or field, Space does what it always does there. |
| F | Finish and log the phase, while focus is inside the session. Focus then lands on Start. |
| Esc | In the Abandon question: keep going, and focus returns to Abandon. |
| EnterSpace | On the task button: open the task picker (type to filter, ↑ ↓ to move, Enter to choose, Esc to close). |
| Left arrowRight arrow | On a length slider in the settings: one step shorter or longer. |
- Task, intention, clock and controls are real controls in reading order. The task button is named with its value (“Task: RC-128, Fix test-fixer… Change task”) and opens a dialog popover with a searchable list.
- The clock is role="timer", labelled by the phase chip, and reads in words (“24 minutes 59 seconds left”). The square around it is decorative; the cycle squares say “2 of 4 focus sessions done, number 3 in progress” to screen readers.
- Start, pause, resume, the 5 and 1 minute marks and the end are announced by the provider, once each. Finish shows a toast with Undo; Abandon logs nothing and says so.
- Abandon never uses a browser confirm box: the question replaces the buttons, focus moves to “Keep going” (the safe choice), and Esc means keep going.
- Focus is never lost when buttons swap: after Finish, Skip break or Abandon it moves to Start; after “Keep going” it returns to Abandon.
- Settings sliders say their unit (“25 minutes”), and the two switches explain themselves: nothing starts by itself unless asked, and the chime plays only after Start.
Guidelines
Do
- Wrap it in a container with overflow-hidden and isolate: the soft halo behind the clock reaches about 80px past the square.
- Keep at least 340px of width for the clock; it shrinks to fit below that.
- Put it under the same FocusProvider as the log and any widget, so they all show one session.
Don’t
- Don’t swap the inline Abandon question for a confirm() box; the inline one already handles focus and Esc.
- Don’t turn auto-start on by default; people should start each phase themselves.
In use
Related
- TimerThe Focus screen: a focus session, a stopwatch and a countdown one switch apart, with the time log beside them.
- Focus widgetThe focus session for a dashboard: choose a task, start or pause, see the time left in the square and today’s total.
- Time logWhat was focused on, by day with daily totals and a seven-day chart, with time added by hand and Undo on delete.
- Inline confirmA question asked in place of the buttons, never with window.confirm: “Abandon this session?” with Keep going first.
- Progress squareProgress drawn as the brand’s square, closing clockwise from the top centre, and a row of squares for a set of focus sessions.
- Time displayThe clock face: Geist Mono digits in fixed cells, lit like the buttons, still or live, with a spoken form for screen readers.
- PickerA filterable list of options, on its own or in a popover: type to filter, arrows and Enter to choose, or 1–9 to pick at once.
Source
The component’s own files, exactly as they run on this site.
/** * A focus session (Pomodoro): pick a task, say what you intend to finish, and work in phases: * Focus → short break → Focus … → long break after four. All lengths are adjustable. * * Past zero a phase keeps counting as overtime ("+02:13") until you finish it, unless "start the next phase by * itself" is on. Finish logs the time (toast with Undo); Abandon asks inline first and logs nothing. * * Keys (while focus is inside the session): Space start / pause · F finish · Esc closes the Abandon question. */import { useEffect, useId, useRef, useState, type CSSProperties, type RefObject } from "react"import { cx } from "../../components/cx"import { statusById } from "../data/sample"import { Icon } from "../icons/Icon"import { formatDuration, formatTime } from "../lib/dates"import { useHotkeys } from "../lib/hotkeys"import { Button, IconButton } from "../ui/button"import { Field, Switch, TextInput } from "../ui/fields"import { PickerButton } from "../ui/picker"import { Popover, usePopover } from "../ui/popover"import {