Skip to content

PlTimePicker

A time of day, chosen from columns. Columns because they are the shape that answers what a time picker is actually asked.

React
tsx
import { PlTimePicker } from 'plass-ui';

<PlTimePicker label="Doors" placeholder="Pick a time" minuteStep={15} />;
dart
import 'package:plass_ui/plass_ui.dart';

PlTimePicker(
  label: const Text('Doors'),
  minuteStep: 15,
  value: doors,
  onChanged: (DateTime? next) => setState(() => doors = next),
);

The columns lift themselves out of the tree, so a picker needs an Overlay above it.

Props

PropTypeDefaultDescription
valueDate | nullThe chosen time. A Date, so it carries a day as well — see referenceDate
defaultValueDate | nullThe time the picker starts on
onValueChange(value: Date | null) => voidCalled with the new value
referenceDateDatetodayThe day a chosen time is written onto while there is no value yet. Held still for as long as the picker is mounted, so a popup left open across midnight does not move the value onto a new day
minTimeDate | nullThe earliest time of day that may be chosen. Only the clock is read
maxTimeDate | nullThe latest time of day that may be chosen
hour12booleanA 12-hour dial with an AM/PM column. Defaults to whatever the locale does
showSecondsbooleanfalseAdds the seconds column
hourStepnumber1How far apart the rows of each column are
minuteStepnumber1See hourStep
secondStepnumber1See hourStep
shouldDisableTime(value: Date, unit: TimeUnit) => booleanBlocks individual rows. Called once per row per column with the instant that row would produce and the column it is in, so a rule may be as coarse as "no afternoons" or as fine as one minute
showNowButtonbooleantrueOffers the shortcut to the current time in the footer
variantshared'solid' | 'glass' | 'ghost''glass'The material of the trigger, drawn on PlTextField's shell. solid is the well cut into the sheet
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Height and type scale
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. Arbitrary colour values are not accepted
densityshared'default' | 'compact''default'Padding only — never the height, never the type scale
elevationshared0 | 1 | 2 | 30Drop shadow depth of the trigger. The popup has its own, fixed at 3 — it genuinely floats
openbooleanWhether the popup is open. Use with onOpenChange to control it
defaultOpenbooleanfalseWhether the popup starts open
onOpenChange(open: boolean) => voidCalled when the popup opens or closes
localestringBCP 47 tag deciding the month and weekday names, the order of the header's two buttons, and how the trigger writes the value. Defaults to the browser's
formatIntl.DateTimeFormatOptions{ hour: 'numeric', minute: '2-digit' }How the trigger writes the chosen time. Passed straight to Intl; seconds are added when showSeconds is on
placeholderReactNodeShown in the trigger while nothing is chosen
clearablebooleanfalseOffers the × that empties the control
closeOnSelectbooleanfalseCloses the popup as soon as any column is touched. false by default, unlike PlDatePicker, because a time is two answers and closing after the first would make choosing 9:30 a matter of opening the popup twice
labelsPartial<PlPickerLabels>The strings the picker says on its own behalf. Every one has an English default. The dates are never in here: Intl knows those
labelReactNodeLabel above the trigger
descriptionReactNodeHelper text below the trigger
errorReactNodeError message below. Its presence also turns the control invalid
invalidbooleanForces the invalid state without a message
startIconReactNodeThe glyph before the value. A calendar, or a clock, by default
fullWidthbooleanfalseStretches to the width of the container
readOnlybooleanfalseThe value is shown but cannot be changed, and the popup does not open
disabledbooleanfalseUnavailable
requiredbooleanfalseWhether a value must be chosen before the form is submitted
namestringIdentifies the field when a form is submitted, as HH:MM — HH:MM:SS when showSeconds is on
classNames{ label?, control?, description?, error?: string }Classes on the parts a className does not reach. control is the part a reader acts on
PropTypeDefaultDescription
value * DateTime?The chosen time. A DateTime, so it carries a day as well
onChangedValueChanged<DateTime?>?Called with the time that was chosen, or null when the picker is emptied
openbool?Whether the popup is open. Use with onOpenChange to control it
onOpenChangedValueChanged<bool>?Called when the columns should open or close
referenceDateDateTime?nowThe day a chosen time is written onto while there is no value yet. Held still for as long as the picker is mounted, so a popup left open across midnight does not move the value onto a new day
minTimeDateTime?The earliest time of day that may be chosen. Only the clock is read
maxTimeDateTime?The latest time of day that may be chosen
hour12boolfalseA 12-hour dial with an AM/PM column. A plain false where React takes it from the locale: there is no Intl here to ask, and the words it uses when on are PlDateNames.am and .pm
showSecondsboolfalseAdds the seconds column
hourStepint1How far apart the rows of each column are
minuteStepint1See hourStep
secondStepint1See hourStep
shouldDisableTimebool Function(DateTime value, PlassTimeUnit unit)?Blocks individual rows. Called once per row per column with the instant that row would produce and the column it is in, so a rule may be as coarse as "no afternoons" or as fine as one minute
namesPlDateNamesPlDateNames.englishWhere AM and PM come from
labelsPlPickerLabelsPlPickerLabels.englishThe strings the picker says on its own behalf. Every one has an English default. The dates are never in here: Intl knows those
formatValueString Function(DateTime value)?How the trigger writes the chosen time. Without it, H:MM — with seconds and a meridiem when those are on
placeholderWidget?Shown in the trigger while nothing is chosen
clearableboolfalseOffers the × that empties the control
showNowButtonbooltrueOffers the shortcut to the current time in the footer
closeOnSelectboolfalseCloses the popup as soon as any column is touched. false by default, unlike PlDatePicker, because a time is two answers and closing after the first would make choosing 9:30 a matter of opening the popup twice
variantsharedPlassVariantPlassVariant.glassThe material of the trigger, drawn on PlTextField's shell. solid is the well cut into the sheet
sizesharedPlassSizePlassSize.mdHeight and type scale
colorsharedPlassColorPlassColor.primarySemantic colour role. Arbitrary colour values are not accepted
densitysharedPlassDensityPlassDensity.standardPadding only — never the height, never the type scale
elevationsharedint0Drop shadow depth of the trigger. The popup has its own, fixed at 3 — it genuinely floats
labelWidget?Label above the trigger
descriptionWidget?Helper text below the trigger
errorWidget?Error message below. Its presence also turns the control invalid
invalidbool?Forces the invalid state without a message
startIconWidget?The glyph before the value. A calendar, or a clock, by default
fullWidthboolfalseStretches to the width of the container
readOnlyboolfalseThe value is shown but cannot be changed, and the popup does not open
disabledboolfalseUnavailable
semanticLabelString?The name a screen reader gives a trigger with no visible label
focusNodeFocusNode?Drive focus from outside
autofocusboolfalseTakes focus as it is inserted into the tree

Every native <div> attribute passes straight through to the field wrapper. color is excluded because it collides with the color in the table above, defaultValue because the picker spells it as a value rather than a DOM attribute, and children because the columns are the component.

A className lands on the stack that holds the label, the control and the two lines under it. classNames reaches the four parts inside it: label, control (the trigger) description and error.

The picker is controlled: value with onChanged, and null is a picker with nothing chosen.

Everything a PlDatePicker says about locale and the absence of a date library holds here too. Intl is what decides whether the clock is on a 12-hour dial and what AM and PM are called.

Everything a PlDatePicker says about the absence of a date library holds here too. What differs is that nothing decides the dial for you: hour12 is a plain false by default, and the words it uses when it is on are PlDateNames.am and .pm.

Columns, not a dial

"Half past nine" is two glances at two columns. "Any time at all, on the hour" is a column you never touch. A clock face is prettier, needs a transform to read, and answers neither question faster, and this library does not put a transform on a control.

The chosen row in each column is scrolled into view once, on open. That is not decoration: a column of sixty minutes that opens at 00 while the value is 45 has hidden its own answer.

The bounds

This is the detail that separates a working time picker from a frustrating one. A bound is checked against the span a row stands for, not against one instant inside it.

With a minTime of 09:30, the hour 9 covers 09:00:00–09:59:59, which overlaps what is allowed, so it stays available, and the minute column is where 00 through 25 grey out. Comparing the whole candidate instead hides the 9 and makes half past nine unreachable.

The value

Not a string and not a number of minutes. Everything else in this library that carries a moment is a Date, and a bare time has nowhere to record that it crossed a daylight-saving boundary. referenceDate is the day a bare time is written onto, and it is held still for as long as the picker is mounted. A popup left open across midnight must not quietly move the value onto a new day.

Examples

hour12

Taken from the locale unless you say otherwise.

false unless you say otherwise: there is no Intl to ask what this region does.

A 12-hour dial reads 12, 1, 2 … 11 rather than 0, 1, 2, and gains an AM/PM column; a 24-hour one runs 00 to 23 and has none.

React

The step intervals

hourStep, minuteStep and secondStep decide how far apart the rows are. A booking that only takes quarter hours should say so with minuteStep={15} rather than by rejecting 09:07 after the fact.

React

minTime · maxTime · shouldDisableTime

minTime and maxTime read the clock only. The day on them is ignored. shouldDisableTime is called once per row per column with the instant that row would produce and the column it is in, so a rule may be as coarse as "no afternoons" or as fine as one minute.

React

closeOnSelect

false here, and true on a PlDatePicker. A day is one answer; a time is two, and closing after the first would make choosing 9:30 a matter of opening the popup twice.

Because the popup stays up while the columns are being read, there has to be something to press that means that is the one, so the footer carries a Done. Turning closeOnSelect on takes it away, since there is then nothing for it to do.

readOnly · disabled · error

React

Accessibility

  • Each column is named after the unit it holds, and each row says whether it is the chosen one.
  • A blocked row stays in its column and is announced as unavailable rather than being removed.
  • Each column is a role="listbox", and each row an option carrying aria-selected; a blocked one carries aria-disabled rather than the attribute.
  • Three unlabelled lists of numbers say nothing to a reader who is not looking at them, so a polite live region beside the columns reads the whole time back as one sentence whenever it changes.
  • The chosen row in each column is brought into view inside its own column, by setting scrollTop rather than calling scrollIntoView, which walks every scrollable ancestor up to the document and, on the frame the popup opens, would scroll the page to the top to reveal a row that is about to move anyway.
  • The trigger is held open at the width of the longest time it could show. Those samples are aria-hidden and drawn as generated content.
  • With name, a hidden input carries the value as a local HH:MM, the shape <input type="time"> submits, so a server that already parses those needs no new code.
  • Each column is a semantics container named after its unit, and each row is announced with the whole of what it means, 14 Hour, not 14.
  • The trigger carries the time as its semantic value rather than folding it into its label.
  • The live region beside the columns reads the whole time back whenever it changes.

Differences from the React build

ReactFlutterWhy
hour12 defaults to the locale's dialit defaults to falseThere is no Intl here to ask. PlDateNames.am / .pm supply the words.
format: Intl.DateTimeFormatOptionsformatValue: String Function(DateTime)The same trade PlDatePicker explains.
role="listbox" and optiona named semantics container and rows that say what they meanFlutter names the state on the node itself.
the hidden input, nameThere is no native form submission to be part of.
className, style, native attributesThere is no class list and no style attribute to pass through.

Released under the MIT License