# Shadcn Number Input: Stepper, Min, Max und Dezimalzahlen

> Warum type=number als shadcn number input scheitert (Scrollen, Pfeile, Kommas) und wie du ihn mit Steppern, Gedrückthalten, min, max, step und Intl baust.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 6. Nov. 2026
- Kategorie: [Komponenten-Guides](https://wingo-ui.com/de/blog/category/components)
- Lesezeit: 8 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/shadcn-number-input
- Englische Fassung: https://wingo-ui.com/blog/shadcn-number-input

## Kurz gesagt

shadcn/ui hat Stand November 2026 keinen Number Input, und der übliche Ersatz, ein Input mit type="number", ändert sich beim Scrollen, zeigt winzige Pfeile, lehnt in vielen Setups ein Dezimalkomma ab und liefert dir bei halb getippten Werten einen leeren String. Ein besserer shadcn number input ist ein Textfeld mit inputMode="decimal", Parsing nach Locale, Buttons für + und -, die beim Gedrückthalten wiederholen, min, max und step, die beim Verlassen greifen, und Intl-Formatierung im Ruhezustand. Der Number Input von Wingo UI (Pro) kann all das, und der kostenlose Input mit den number-format-Helfern deckt ein einfacheres Feld ab.

Du brauchst ein Feld für eine Menge, ein Gewicht oder einen Preis, suchst nach einem shadcn number input und stellst fest: Es gibt keinen. Die Liste von shadcn/ui hat Input, Input Group und Field (geprüft im November 2026), also greifst du zu `<Input type="number" />`. In der Demo klappt das, dann kommen echte Nutzer: Jemand scrollt über dem fokussierten Feld, und der Preis sinkt um 3. Jemand anderes tippt `2,5` in einem Browser, der das Komma ablehnt, und dein State bekommt `""` statt einer Zahl. Dieser Guide zeigt, was `type="number"` falsch macht, was ein gutes Zahlenfeld stattdessen tut und wie du eins baust: mit dem [Number Input](https://wingo-ui.com/components/number-input) von Wingo UI oder mit dem kostenlosen [Input](https://wingo-ui.com/components/input), wenn dir die Grundlagen reichen.

## Warum enttäuscht type="number" in einem shadcn-Formular?

Weil es für Werte gedacht ist, die man mit einem Spinner hoch- und runterzählt, während die meisten Formulare Werte abfragen, die Leute eintippen. Der Hinweis zur Barrierefreiheit bei MDN rät selbst, darauf zu verzichten, wenn der Spin-Button keine Rolle spielt, und warnt davor, dass Nutzer eine Zahl versehentlich erhöhen ([MDN, input type="number"](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/number), gelesen im November 2026). Das Team des GOV.UK Design System ist 2020 davon abgerückt, nachdem es Probleme bei der Bedienbarkeit gefunden hatte, darunter Bugs mit den assistiven Technologien Dragon und NVDA ([GDS-Blog, Februar 2020](https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/)). Diese Probleme triffst du in einer React-App:

1. **Das Mausrad kann den Wert ändern.** Du fokussierst das Feld, scrollst, um den Rest des Formulars zu lesen, und in manchen Browsern verschiebt sich die Zahl. Das merkt niemand, bis die Bestellung falsch ist.
2. **Die Pfeile sind winzig.** Die nativen Spin-Buttons sind auf dem Desktop ein kleines Pfeilpaar und fehlen in Browsern auf dem Smartphone meist ganz. Als Hauptbedienung taugen sie also nicht, und auf dem Desktop machen sie das Feld trotzdem unruhig.
3. **Dezimalzahlen hängen von Browser und step ab.** Mit dem Standardwert `step="1"` ist `2.5` ungültig. Ein Number Input mit Komma als Dezimaltrennzeichen klappt in manchen Browsern für manche Locales, in anderen nicht.
4. **Der Wert ist ein String, und manchmal ist er leer.** Solange der Text keine gültige Zahl ist (`-`, `1e`, `2,5`, wo das Komma nicht akzeptiert wird), ist `event.target.value` gleich `""`. Das Feld zeigt den Text weiter an, aber dein State, deine Validierung und alles, was daraus abgeleitet ist, sehen ein leeres Feld.
5. **Keine Formatierung.** `12,500` oder `2.5 kg` kannst du darin nicht anzeigen, nur die rohen Ziffern.
6. **min und max verhindern kein Tippen.** Sie begrenzen nur die Pfeile. Eine getippte `5000` in einem Feld mit `max="100"` bleibt stehen und wird von der Constraint Validation als ungültig markiert. Code, der den Wert aus dem State liest, oder ein Formular mit `noValidate` bekommt ihn trotzdem.

## Wie entferne ich die Pfeile aus einem shadcn number input?

Wenn du bei `type="number"` bleibst, entfernst du die Pfeile mit einer Arbitrary Property und zwei Arbitrary Variants von Tailwind v4:

```tsx
<input
  type="number"
  className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
/>
```

`appearance: textfield` blendet die Pfeile des shadcn number input in Firefox aus, die beiden WebKit-Pseudo-Elemente erledigen das in Chrome und Safari. Damit ist nur die Optik repariert. Mausrad, ArrowUp und ArrowDown ändern den Wert weiterhin, und die Probleme mit Strings und Dezimalzahlen bleiben. Wenn du die Pfeile ohnehin entfernst, ist es sauberer, `type="number"` ganz aufzugeben.

## Was sollte ein guter Number Input stattdessen tun?

Nimm einen Text-Input mit Zifferntastatur und gib ihm das Verhalten, das Leute von einem Stepper erwarten:

- **Ein Text-Input mit `inputMode`.** `inputMode="decimal"` öffnet auf dem Smartphone eine Zifferntastatur mit Dezimaltaste, `numeric`, wenn nur ganze Zahlen sinnvoll sind. Keine Pannen mit dem Mausrad, keine winzigen Pfeile.
- **Parsing in der Locale der Person.** `1,234.5` in en-US und `1.234,5` in de-DE sind dieselbe Zahl, und `2,5` von einer Smartphone-Tastatur mit Komma sollte funktionieren.
- **Echte Buttons für + und -.** Groß genug zum Antippen (44px auf Touchscreens), mit Wiederholung beim Gedrückthalten, die nach einer kurzen Pause startet und schneller wird.
- **Tastatur wie bei einem Spinbutton.** ArrowUp und ArrowDown ändern um einen Schritt, PageUp und PageDown um einen großen Schritt, Home und End springen zu min und max, dazu `role="spinbutton"` und `aria-valuenow`, damit Screenreader den Wert ansagen.
- **min, max und step beim Übernehmen.** Lass freies Tippen zu, zeig einen Fehlerring, solange der Wert außerhalb des Bereichs liegt, und begrenze und runde auf den Schritt, wenn das Feld den Fokus verliert.
- **Formatierung im Ruhezustand.** Zeig `$485,000` oder `2.5 kg`, solange das Feld inaktiv ist, und die reine Zahl beim Bearbeiten.
- **Eine Zahl als Ergebnis, kein Text.** Dein State bekommt `85` oder `null`, nie `"85 m²"`.

## Wie baue ich das mit dem Number Input von Wingo UI?

Installier ihn mit der CLI. Die Field-Anatomie und die number-format-Helfer kommen gleich mit:

```bash
npx wingo-ui@latest add number-input
```

Probier die Demo unten aus: Tipp einen Wert über 1,000 ein, drück dann Tab und sieh zu, wie er zurückspringt. Halt den Button + gedrückt, dann merkst du, wie die Wiederholung schneller wird.

> Live-Demo (Number Input): Tipp 2000 bei Floor area ein und drück Tab: Der Wert wird auf 1,000 begrenzt. Halt + bei Rooms gedrückt, bis der Wert bei 10 stehen bleibt, und scroll dann über dem fokussierten Feld, um ihn schrittweise zu ändern. Probier sie unter [Number Input](https://wingo-ui.com/components/number-input) aus und installier sie mit `npx wingo-ui@latest add number-input`.

Die wichtigen Props sind `min`, `max`, `step`, `decimals`, `steppers` und `format`. Hier drei typische Felder: eine Menge, ein Gewicht mit einer Nachkommastelle und ein Preis:

```tsx
"use client";

import { useState } from "react";
import { NumberInput } from "@/components/ui/number-input";

export function ShipmentDetails() {
  const [boxes, setBoxes] = useState<number | null>(2);
  const [weight, setWeight] = useState<number | null>(12.5);

  return (
    <form className="flex flex-col gap-4">
      <NumberInput
        name="boxes"
        label="Pakete"
        steppers="split"
        min={1}
        max={20}
        value={boxes}
        onValueChange={setBoxes}
        locale="en-US"
      />
      <NumberInput
        name="weight"
        label="Gewicht pro Paket"
        step={0.1}
        min={0.1}
        max={70}
        format={{ style: "unit", unit: "kilogram" }}
        value={weight}
        onValueChange={setWeight}
        locale="en-US"
      />
      <NumberInput
        name="declaredValue"
        label="Warenwert"
        currency="USD"
        decimals={0}
        step={50}
        min={0}
        defaultValue={1200}
        onValueCommit={(value) => console.log("Gespeichert:", value)}
        locale="en-US"
      />
    </form>
  );
}
```

Ein paar Details, die du kennen solltest:

- **`step` legt auch die Genauigkeit fest.** `step={0.1}` behält eine Nachkommastelle und schaltet die Smartphone-Tastatur auf Dezimal um. Ein React Number Input mit Dezimalzahlen braucht also kein zusätzliches Prop. `decimals` überschreibt das, wenn Schritt und Genauigkeit auseinandergehen.
- **Schritte zählen ab `min`.** Mit `min={1}` und `step={5}` geht + auf 1, 6, 11. Ein Wert, der nicht auf dem Raster liegt, springt zuerst auf den nächsten Rasterwert.
- **`clampBehavior` bestimmt, wie streng min und max sind.** Der Standard `blur` lässt dich außerhalb des Bereichs tippen, zeigt den roten Ring und begrenzt beim Verlassen. `strict` verweigert einen Tastendruck, der den Wert nur weiter aus dem Bereich schieben kann. `none` begrenzt nie, für den Fall, dass dein Schema validiert.
- **`onValueChange` oder `onValueCommit`.** Der erste feuert bei jedem Tastendruck und jedem Stepper-Schritt. Der zweite feuert einmal pro Bearbeitung (beim Verlassen, bei Enter, beim Loslassen eines gehaltenen Steppers). Dorthin gehört ein fetch oder eine Neuberechnung des Preises.
- **Das Mausrad lässt sich abschalten.** Es ändert den Wert nur, solange das Feld fokussiert ist und der Mauszeiger darüber liegt, und die Seite scrollt in diesem Moment nicht. Übergib `wheel={false}` für Felder in langen Formularen, durch die man scrollt.
- **`name` sendet eine Zahl.** Ein versteckter Input trägt `"12.5"` mit Dezimalpunkt. `FormData` und Server Actions bekommen also einen Wert, den du an `Number()` übergeben kannst, egal in welcher Locale das Feld anzeigt.

Dasselbe Feld parst so, wie die Locale Zahlen schreibt. Hier läuft es auf Deutsch mit halben Schritten, also tipp `12,5` und drück Tab:

> Live-Demo (Number Input): In de-DE: Tipp 12,5 und drück Tab, im Ruhezustand steht dann 12,5 m² da. Drück zweimal ArrowUp, jedes Mal kommen 0.5 dazu. Probier sie unter [Number Input](https://wingo-ui.com/components/number-input) aus und installier sie mit `npx wingo-ui@latest add number-input`.

## Ist er barrierefrei und auf dem Smartphone gut bedienbar?

Das Feld ist ein echtes `<input>` mit `role="spinbutton"`, `aria-valuenow`, `aria-valuemin`, `aria-valuemax` und einem `aria-valuetext`, das den formatierten Wert vorliest. Ein Screenreader sagt also „85 m²“ statt „85“. Die Buttons für + und - haben zugängliche Namen (`incrementLabel` und `decrementLabel`, standardmäßig „Increase“ und „Decrease“) und werden an den Grenzen gedimmt. Label, Beschreibung und Fehler kommen aus der gemeinsamen [Field](https://wingo-ui.com/components/field)-Anatomie, verknüpft über `htmlFor` und `aria-describedby`, sodass sich das Feld wie jeder andere Input ins Formular einfügt.

Auf dem Smartphone ist der Text 16px groß, damit iOS beim Fokus nicht zoomt, die Box in `md` wächst auf 44px, und jeder Stepper bekommt eine Trefferfläche von 44px. `steppers="split"` setzt - nach links und + nach rechts, den Wert in die Mitte. Für Mengen unter dem Daumen funktioniert dieses Layout am besten. Übereinanderliegende Chevrons mit `stacked` sind eine Desktop-Gewohnheit; auf Touchscreens werden daraus zwei Buttons von 44px nebeneinander.

## Wann ist ein Currency Input das bessere Feld?

Wenn die Zahl Geld ist und sich die Währung ändern kann, oder wenn Leute große Beträge tippen. Ein Number Input mit `currency="USD"` reicht für einen Preis in fester Währung, den man meist per Stepper ändert. Ein Geldfeld, in das Leute tippen, braucht mehr: Tausendertrennzeichen schon beim Tippen, ohne dass der Cursor springt, Auffüllen auf die Nachkommastellen der Währung (2 für USD, 0 für JPY, 3 für BHD), optional eine Währungsauswahl und Chips für schnelle Beträge. Das ist der [Currency Input](https://wingo-ui.com/components/currency-input):

> Live-Demo (Currency Input): Klick in den Betrag, tipp 1234.5 und drück Tab: Er wird auf 1,234.50 aufgefüllt. Öffne den Währungs-Button und wähl einen anderen Code: Der Betrag wird nicht umgerechnet. Probier sie unter [Currency Input](https://wingo-ui.com/components/currency-input) aus und installier sie mit `npx wingo-ui@latest add currency-input`.

```tsx
import { CurrencyInput } from "@/components/ui/currency-input";

<CurrencyInput
  name="amount"
  currencyName="currency"
  label="Betrag"
  locale="en-US"
  currencies={["USD", "EUR", "GBP"]}
  defaultValue={250}
  min={1}
/>
```

Die Cursor-Mathematik und das Parsing dahinter erklärt der [Guide zum React Currency Input](https://wingo-ui.com/de/blog/react-currency-input) Schritt für Schritt.

## Bekomme ich ein ordentliches Zahlenfeld nur mit kostenlosen Komponenten?

Ja, wenn du ohne Stepper und ohne Wiederholung beim Gedrückthalten auskommst. Der kostenlose Input behandelt `type="number"` bereits als Textfeld mit Dezimaltastatur, das Mausrad ändert ihn also nie. Die kostenlosen [number-format-Helfer](https://wingo-ui.com/components/number-format) liefern dir Parsing nach Locale, Begrenzen und Runden auf den Schritt:

```tsx
"use client";

import { useState } from "react";
import { Input } from "@/components/ui/input";
import { clamp, formatNumber, parseNumber, snapToStep } from "@/lib/number-format";

export function WeightField({ locale = "en-US" }: { locale?: string }) {
  const [text, setText] = useState("12.5");

  // eingabe parsen, begrenzen, auf 0.1 runden und formatiert zurückgeben
  function commit() {
    const parsed = parseNumber(text, locale);
    if (parsed === null) return setText("");
    const value = clamp(snapToStep(parsed, 0.1), 0.1, 70);
    setText(formatNumber(value, locale, { maximumFractionDigits: 1 }));
  }

  return (
    <Input
      type="number"
      name="weight"
      label="Gewicht pro Paket"
      suffix="kg"
      value={text}
      onValueChange={setText}
      onBlur={commit}
    />
  );
}
```

Installier beides mit `npx wingo-ui@latest add input number-format`. Parse `text` in deinem Submit-Handler noch einmal, oder halte eine separate Zahl im State. Brauchst du Stepper, die Tastenbelegung und die formatierte Anzeige, spart dir der Number Input aus Pro an dieser Stelle ein paar hundert Zeilen.

## Was sind die typischen Stolperfallen?

- **Den formatierten String speichern.** Halte `number | null` im State und im Schema, und formatiere nur für die Anzeige.
- **Bei jedem Tastendruck begrenzen.** Mit `max={100}` wird `150` auf dem Weg zu `15` auf `100` begrenzt, und die Eingabe ist weg. Begrenze beim Verlassen des Felds.
- **Rundungsfehler bei Schritten.** `0.1 + 0.2` ergibt `0.30000000000000004`. Runde nach jedem Schritt auf die Nachkommastellen des Schritts (`roundTo` in number-format erledigt das).
- **Einen Number Input für IDs verwenden.** Postleitzahlen, Kartennummern und Telefonnummern sind Ziffernfolgen, keine Mengen. Nimm einen normalen Text-Input mit `inputMode="numeric"` und einem Wert für `autoComplete`.

Für den Rest des Formulars, also Labels, Fehler und Validierung mit Zod, lies den [Guide zu React-Formularkomponenten](https://wingo-ui.com/de/blog/react-form-components-guide) und die anderen Beiträge in den [Komponenten-Guides](https://wingo-ui.com/de/blog/category/components). Wenn du Number Inputs auf einer ganzen Seite sehen willst: Die [Vorlage ROI Calculator](https://wingo-ui.com/templates/roi-calculator-page) verbindet mehrere davon mit einem Live-Ergebnis.

## Wie geht es weiter?

Fang mit dem kostenlosen Input und den number-format-Helfern an, wenn ein einfaches Zahlenfeld reicht: Mit einem kostenlosen Konto (nur E-Mail) bekommst du den Quellcode. Brauchst du Stepper, Wiederholung beim Gedrückthalten und Intl-Formatierung, sind Number Input und Currency Input Teil von Pro; die Preise stehen auf der [Preisseite](https://wingo-ui.com/de/pricing).

## Komponenten in diesem Artikel

- [Number Input](https://wingo-ui.com/components/number-input) (Pro): A React number input to type or step values: steppers with hold to repeat, min, max and step, and Intl formatting for money and units. Installation: `npx wingo-ui@latest add number-input`
- [Currency Input](https://wingo-ui.com/components/currency-input) (Pro): A React currency input: a money field formatted per locale as you type, with an optional currency selector and quick amount presets. Installation: `npx wingo-ui@latest add currency-input`
- [Input](https://wingo-ui.com/components/input) (Free): A React text input: icons and addons inside the box, a clear button, a loading spinner, a character counter and floating labels. Installation: `npx wingo-ui@latest add input`
- [Field](https://wingo-ui.com/components/field) (Free): A React form field wrapper every control shares: label, hint, animated error and counter, fieldsets, and the box recipe all inputs use. Installation: `npx wingo-ui@latest add field`

## Häufige Fragen

### Hat shadcn/ui einen Number Input?

Nein. Stand November 2026 enthält die offizielle Komponentenliste von shadcn/ui Input, Input Group, Input OTP und Field, aber keinen Number Input und kein Number Field. Die meisten Projekte nehmen Input mit type="number" oder eine Komponente aus der Community.

### Wie entferne ich die Pfeile aus einem shadcn number input?

Ergänze im className des Inputs [appearance:textfield] für Firefox und [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none für Chrome und Safari. Mausrad und Pfeiltasten ändern den Wert trotzdem, also überleg dir stattdessen einen Text-Input mit inputMode="decimal".

### Wie erlaube ich Dezimalzahlen in einem React Number Input?

Bei type="number" setzt du step="any" oder einen Dezimalschritt wie 0.01, sonst markiert der Browser 2.5 als ungültig. Bei einem Text-Input nimmst du inputMode="decimal" und parst den Text selbst, damit 2.5 und 2,5 beide funktionieren.

### Wie setze ich min und max bei einem React Number Input?

Die nativen Attribute min und max bremsen nur die Pfeile. Ein getippter Wert außerhalb des Bereichs wird trotzdem angenommen und nur als ungültig markiert. Begrenz die geparste Zahl beim Verlassen des Felds und dimm den Button für + oder -, sobald der Wert eine Grenze erreicht.

### Warum liefert mein React Number Input einen String?

Der Wert jedes Inputs ist ein String, und bei type="number" ist er leer, solange der Text noch keine gültige Zahl ist, etwa "1e" oder "-". Nimm event.target.valueAsNumber oder parse den String, und speichere null für ein leeres Feld.

---

Quelle: https://wingo-ui.com/de/blog/shadcn-number-input
