Skip to content

NumberPicker

NumberPicker (from scenerystack/sun) shows a number flanked by an increment arrow above and a decrement arrow below, both drawn directly on the top and bottom halves of the value's own rounded background — the compact, classic "PhET number picker" look used throughout older sims. It's bound to a Property<number> and a TReadOnlyProperty<Range>, exactly like NumberSpinner. Despite the different name, NumberPicker and NumberSpinner are not distinguished by accessibility — both mix in the same AccessibleNumberSpinner, so both support keyboard arrow-key/PDOM interaction out of the box. The real difference is visual construction and configurability: NumberSpinner composes separate ArrowButton instances positioned around a NumberDisplay (configurable via arrowsPosition — above/below, beside, or both-right/both-bottom), while NumberPicker's arrows are painted directly onto the two halves of its own background shape, with less layout flexibility but a smaller footprint.

ts
import { NumberPicker } from 'scenerystack/sun';
import { NumberProperty, Property } from 'scenerystack/axon';
import { Range } from 'scenerystack/dot';
import { Tandem } from 'scenerystack/tandem';

const countProperty = new NumberProperty( 3, {
  numberType: 'Integer',
  range: new Range( 0, 10 )
} );

const countRangeProperty = new Property( new Range( 0, 10 ) );

const countPicker = new NumberPicker( countProperty, countRangeProperty, {
  tandem: Tandem.REQUIRED
} );

As with NumberSpinner, the range argument is a TReadOnlyProperty<Range> (not a plain Range), so the picker can respond if the allowed bounds change dynamically.

Options

OptionDefaultEffect
color / pressedColorblue / a darker derived shadeArrow and background-gradient color, normal and while pressed
backgroundColor'white'Background color when the pointer isn't over it
cornerRadius6Corner radius of the value background
xMargin / yMargin3 / 3Margin between the value text and the background edges
decimalPlaces0Digits shown after the decimal point (via the default formatValue)
fontPhetFont(24)Font for the displayed value
incrementFunction / decrementFunctionv => v + 1 / v => v - 1Custom step logic, e.g. non-uniform steps
align'center'Horizontal alignment of the value text: 'center', 'left', or 'right'
touchAreaXDilation / touchAreaYDilation10 / 10Expand each arrow's touch hit area
mouseAreaXDilation / mouseAreaYDilation0 / 5Expand each arrow's mouse hit area
incrementEnabledFunction / decrementEnabledFunctionDisables at range.max/range.minPredicates controlling when each arrow is enabled
formatValuepads to decimalPlaces( value: number ) => string — custom value formatting
onInputno-op( event: SceneryEvent | null, oldValue: number ) => void, called on any user-driven value change
valueChangedSoundPlayer / boundarySoundPlayershared default sound playersSounds for an ordinary change vs. hitting the range's min/max

Methods

MethodEffect
setArrowsVisible( visible )Shows/hides both arrows, interrupting any in-progress press when hidden
NumberPicker.createIcon( value, options? )Static — builds a non-interactive NumberPicker instance for use as an icon, with pickable: false and removed from the PDOM by default

Arrows disable themselves automatically at the range's edges — same as NumberSpinner

NumberPicker derives incrementEnabledProperty/decrementEnabledProperty from incrementEnabledFunction/decrementEnabledFunction and keeps each arrow's pickable state (and its keyboard equivalent) in sync automatically. If you need non-default enabling logic — e.g. disabling early before the true range boundary — override these two functions rather than trying to intercept onInput.