<!-- GENERATED by scripts/build-llms.mjs from llms/forms.md — do not edit this file. -->

# `lr-number-input`

- **Import** `import '@aceshooting/lyra-ui/components/lr-number-input.js';` (stable tag alias; registers the tag)
- **Class** `LyraNumberInput`, also available unregistered from `@aceshooting/lyra-ui/components/forms/input/number-input.class.js`
- **Family** `components/forms/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 23 parts, 22 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-number-input`

A numeric field with the complete `lr-input` form, validation and native-editing contract, plus its
own increment/decrement stepper pair. Its constructor and `connectedCallback()` both force
`type = 'number'`; all inherited `lr-input` form and editing APIs remain available.

`stepUp()` and `stepDown()` use the current `value`, `step`, `min`, and `max` properties in the same
synchronous call, including changes made immediately beforehand. They preserve native step alignment
and bounds, update form submission silently, and remain no-ops before the native input has rendered
or while disabled/readonly. Native `step="any"` remains a non-steppable no-op.

Changing own `disabled` from true to false in the same task that disables an ancestor fieldset keeps
the native editing control effectively disabled. The enabled first-legend exception and explicit
own-disabled state retain their native meaning; validity and form submission follow the effective
disabled state.

**Inherits:** all public surface from `lr-input`.

The inherited `--lr-input-*` theme inputs keep `lr-input`'s ancestor-theme precedence; the number
subclass does not redeclare them on its host.
It also installs the shared six-tier size sheet: stepper-bearing rows follow the same rendered
action-height ladder as `lr-input` instead of remaining at the default tier for every `size`.

**Properties:** `size` (`2xs`…`xl`), `appearance`, `pill`, `autofocus`, `placeholder`, `readonly`,
`label`, `hint`, `errorText`
(`error-text`), `accessibleLabel` (`aria-label`), `autocomplete`, `spellcheck`, `autocapitalize`,
`autocorrect` (boolean readback; accepts `boolean | 'off' | 'on'` writes), `inputMode`
(`inputmode`), `enterKeyHint` (`enterkeyhint`), and
`min`/`max`/`step` (the native numeric constraint validation), all inherited from `lr-input` with
identical meaning. This component changes the mapped defaults to `appearance='outlined'`,
`inputMode='numeric'`, and `step=1`. `clearable` (and its `with-clear` spelling),
`passwordVisible` (`password-visible`), and `minlength`/`maxlength`/`pattern` are inherited but
inert — see gotchas. Also inherited from `lr-input`, with identical meaning:
`defaultValue: string = ''` (attribute `value`, reflected) — the reset value; and
`customError: string | null = null` (attribute `custom-error`, reflected) — a consumer-supplied
validation message. `getForm()` returns the browser-resolved form owner, including an external
owner selected by `form`; `setCustomValidity(message)` sets or clears `customError` without
discarding intrinsic validity; `resetValidity()` clears only that consumer layer and recomputes
the current intrinsic constraints, leaving `value`/`defaultValue` and prior interaction state
unchanged.

Stepper switches:

- `steppers: boolean = true` (attribute `steppers`, reflected) — renders the increment/decrement
  pair inside the control row. It **defaults to `true`**, so it needs a custom converter to switch
  off: write `steppers="false"` as an attribute, or `.steppers=${false}` as a property binding.
  A bare `?steppers=${false}` (or removing the attribute in a framework that models booleans by
  presence) cannot reset a `true`-defaulting property.
- `withoutSpinButtons: boolean = true` (attribute `without-spin-buttons`, reflected) — the same
  knob `lr-input` exposes, but **defaulted the other way here** (`lr-input`'s default is `false`),
  so the component's own steppers are never shown alongside the browser's built-in spin buttons.
  It is `true`-defaulting too, so `without-spin-buttons="false"` / `.withoutSpinButtons=${false}`
  brings the native pair back. The two properties are independent: `steppers="false"
without-spin-buttons="false"` returns the field to a plain native `<input type="number">`.
- `withoutSteppers: boolean = false` (attribute `without-steppers`, not reflected) — the positive
  upstream spelling for hiding the custom pair. It does not invert `steppers`: either
  `without-steppers` or `steppers="false"` hides the same controls, and both unset leaves them on.

Each stepper drives the inherited `stepUp()`/`stepDown()`, so `min`/`max` clamping and decimal
handling stay the platform's. Unlike those silent methods, a stepper **click** is a user edit and
emits the same `input`/`lr-input`/`change`/`lr-change` sequence typing would — but only when the
value actually moved, so clicking at a bound is inert rather than emitting a no-op edit. Clicking
also returns focus to the field.

The steppers are deliberately outside the tab order (`tabindex="-1"`), like the native spin buttons
they stand in for: a keyboard user steps the value with ArrowUp/ArrowDown on the field itself, which
the native `<input type="number">` already handles, so making them tab stops would add two stops per
field for no new capability. Each carries a localized accessible name and the shared
`--lr-icon-button-size` hit-area floor in both axes, and both are disabled while the field is
`disabled` or `readonly`.

**Events:** `input`/`change` (native-style, composed), `lr-input`/`lr-change`
(`detail: { value }`), `focus`/`blur` (re-dispatched bubbling + composed from the internal native
input), and `lr-clear`
(inherited, never fired here). The inherited `lr-invalid` (no detail) fires when a validity check
finds the input invalid. The internal native `beforeinput` is cancelable, bubbles, and composes;
calling `preventDefault()` on the host vetoes the edit before `value` changes. It also inherits `lr-input-settled` (`detail: { value }`, non-cancelable), which fires once
`debounce` ms after the last keystroke when `debounce` is set; see `lr-input`'s own `debounce`
entry for the flush and cancellation rules.

**Slots:** `label`, `hint`, `error`, `start`, `end`, `decrement-icon`, and `increment-icon`.

**CSS parts:** `form-control`, `form-control-label`, `base` (compatibility name for the
control row; use `number-input`), `number-input` (the numeric control row; it is the same node as
`base` and the inherited `input-wrapper` part), `input-wrapper`, `input`, shared `stepper`, mapped
`stepper-decrement` / `stepper-increment`, and compatibility `stepper-down` / `stepper-up` (the two
stepper buttons, rendered only while both switches permit them; they sit side by side
between the built-in clear/password actions and the `end` adornment, and each one rotates the shared
chevron glyph — `[part="stepper-up"] > svg` a quarter turn one way, `[part="stepper-down"] > svg` the
other — so a consumer replacing the button chrome keeps the same up/down orientation), `start`, `end`,
`hint`, `error`, plus the inherited `clear-button` and `password-toggle`, neither of which this
component ever renders.

**Themeable custom properties:** inherited from `lr-input`, identical in meaning —
`--lr-input-control-min-height`, `--lr-input-control-height`, `--lr-input-padding-block`,
`--lr-input-padding-inline`, `--lr-input-font-size`, `--lr-input-gap`, `--lr-input-radius`,
`--lr-input-fill`, and `--lr-input-border-color` (all
but `--lr-input-control-height` and `--lr-input-gap` follow the active `size` tier;
`--lr-input-control-height` stays undeclared until you pin an exact
row height, `--lr-input-fill`/`--lr-input-border-color` swap per `appearance` instead of per tier,
and `--lr-input-gap` — like `--lr-button-gap` — is constant across the ladder). The steppers take their font size from `--lr-input-font-size` and their
minimum box from `--lr-icon-button-size`.
The inherited `--lr-input-focus-border-color` and four `--lr-input-action-*` hooks also apply to
the numeric row and its steppers, so their state paint can be isolated from other form controls.
The exact-320px RTL story keeps long label/hint copy and both fixed-size steppers within the host.

```html
<lr-number-input
  label="Quantity"
  min="0"
  max="99"
  step="1"
  value="1"
></lr-number-input>
<!-- A bare numeric field: no steppers, and the browser's own spinners back: -->
<lr-number-input
  label="Quantity"
  steppers="false"
  without-spin-buttons="false"
></lr-number-input>
<script type="module">
  import "@aceshooting/lyra-ui/components/forms/input/number-input.js";
</script>
```

**Known gotchas:**

- **`steppers` and `without-spin-buttons` both default to `true` here.** Only the literal string
  `"false"` parses as `false`; every other attribute value — including an empty one, and including
  _removing_ the attribute — parses as `true`. So `?attr=${false}` and a removed attribute cannot
  reset either; use the `="false"` attribute value or the `.prop=${false}` property binding. The
  two also serialize differently when reflected: `steppers` is absent while `true` and appears as
  `steppers="false"` while `false`, whereas `without-spin-buttons` appears empty while `true` and is
  absent while `false`. Assert the rendered result, not the attribute's presence.
- `clearable`/`clear-button`/`lr-clear` are inert: the clear action only renders for
  `type="text"`/`"search"`. `password-visible`/`password-toggle` are likewise inert, since the
  toggle only renders for `type="password"`. `minlength`/`maxlength`/`pattern` are inert too — the
  platform ignores all three on `type="number"`; use `min`/`max`/`step` instead.
- `type` is re-forced to `number` on every connect, but a later `el.type = 'text'` on a connected
  element is not reverted — use `lr-input` when the type has to change.
