Components

Stepper

Stepper shows a number between a − and a + button. Each press changes the number by step, within min and max. Holding a button keeps stepping. The timer for the repeat runs in the core, so JS only receives onChange with each new value.

ui-stepper.tsx Edit in Playground Open in new tab
// Stepper: its own value, a controlled one with a fractional step, and its ref.
import { mount, signal, Text, View } from "nsh";
import { Button, Group, Stepper, type StepperRef } from "nsh/ui/mobile";

function Row(props: { label: string; children: any }) {
  return (
    <View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
      <Text>{props.label}</Text>
      {props.children}
    </View>
  );
}

function App() {
  const quantity = signal(1.5);
  const asked = signal("");
  let own!: StepperRef;
  return (
    <View style={{ flex: 1, backgroundColor: "#f5f5f5", padding: 16, paddingTop: 24, gap: 20 }}>
      <Text style={{ fontSize: 22, fontWeight: "700" }}>Stepper</Text>
      <Group footer={<Text>Hold − or + to keep stepping.</Text>}>
        <Row label="Own value (0–10)">
          <Stepper ref={own} defaultValue={3} min={0} max={10} />
        </Row>
        <Row label={`Controlled: ${quantity()}`}>
          <Stepper value={quantity()} min={0} max={5} step={0.5} onChange={(v) => quantity.set(v)} />
        </Row>
        <Row label="Disabled">
          <Stepper defaultValue={2} disabled />
        </Row>
      </Group>
      <Button
        variant="secondary"
        title="Read the first, then set it to 7"
        onPress={async () => {
          const was = await own.value();
          own.setValue(7);
          asked.set(`It was ${was}`);
        }}
      />
      <Text style={{ color: "#737373" }}>{asked()}</Text>
    </View>
  );
}

mount(App);
import { Stepper, type StepperRef } from "nsh/ui/mobile";

#Usage

import { mount, signal, Text, View } from "nsh";
import { Stepper } from "nsh/ui/mobile";

function App() {
  const guests = signal(2);
  return (
    <View style={{ flex: 1, padding: 16, paddingTop: 48, gap: 12 }}>
      <Text>{`${guests()} guests`}</Text>
      <Stepper value={guests()} min={1} max={12} onChange={(v) => guests.set(v)} />
    </View>
  );
}

mount(App);

#Controlled and uncontrolled

  • Controlled: pass value and update it in onChange. The stepper shows value. A press reports the next value with onChange, and the number changes when you set the new value.
  • Uncontrolled: leave out value. The stepper keeps its own value, starting at defaultValue, and reports each change with onChange.
<Stepper defaultValue={5} min={0} max={50} onChange={(v) => console.log("now", v)} />

#Range, step and decimals

  • The value always stays within min and max. If min is larger than max, the two are swapped. A negative step is used as positive.
  • The number is shown with as many decimals as step has: step={0.25} shows 1.75, step={1} shows 2.
  • Values are rounded to that number of decimals, which avoids floating-point drift such as 0.30000000000000004.
  • When min, max or step change, the value is moved back into the new range.
<Stepper value={weight()} min={0} max={5} step={0.5} onChange={(v) => weight.set(v)} />

#Holding a button

A press steps once immediately. If the button is still held after 400 ms, the stepper steps again every 80 ms until the button is released or the value reaches min or max. At a bound, that button is dimmed and disabled.

#Ref methods

Pass a ref to get a StepperRef, which reads and sets the value from your code:

import { mount, View } from "nsh";
import { Button, Stepper, type StepperRef } from "nsh/ui/mobile";

function App() {
  let stepper!: StepperRef;
  return (
    <View style={{ flex: 1, padding: 16, paddingTop: 48, gap: 12 }}>
      <Stepper ref={stepper} defaultValue={3} max={10} />
      <Button
        title="Reset"
        onPress={async () => {
          console.log("was", await stepper.value());
          stepper.setValue(0);
        }}
      />
    </View>
  );
}

mount(App);
MemberDescription
setValue(value: number): voidSets the value, rounded to the step’s decimals and kept within the range, and reports it with onChange as a press would. On a controlled stepper, the number changes when you update value.
value(): Promise<number>The value shown. It is a promise because the value lives in the core, not in JS.
node: YNodeThe stepper’s host node.

#Props

PropTypeDefaultDescription
valuenumber | nullnone (uncontrolled)The value, when the app controls it.
defaultValuenumber0The first value, when the app does not control it.
minnumber0The smallest value.
maxnumber100The largest value.
stepnumber1How much a press changes the value.
disabledbooleanfalseNo presses; both buttons dimmed.
refStepperRef | ((ref: StepperRef) => void)noneReceives the ref.
class, classList, style, testID, onLayoutApplied to the stepper’s root view (the row).

#Events

EventPayloadWhen
onChangenumber: the new valueA press, a held button’s repeat or setValue changed the value. Not sent when the value would not change (at a bound).

#Theming

PartTheme key
Buttonssecondary.background, secondary.active.background while pressed, corner radius radius
− and +secondary.foreground, font.size + 4
Numberforeground, font.size + 1

The buttons are 40 × 40 pt. A disabled button, or one at a bound, is shown at 40% opacity. See Theming.

#Accessibility

The buttons have the button role and the labels “Decrease” and “Increase”. A button at a bound, or on a disabled stepper, is reported as disabled. The number is a text that screen readers read as it is.

  • Group: steppers in settings rows.
  • Input: free numeric input with keyboardType.