HomeDocsComponentsBlocksForm contractValidation
Sheet 01 / 01 · Rev 1.0 · 11 specimens

input/cn

The inputs shadcn/ui does not ship. Every field below is live — type in them, paste into them, break them. The readout underneath shows the value onChange actually emits.

All 11 components
MITvalidation built insmart pasteReact 19zod 40 runtime deps
SPEC-01PHONEINPUT

Paste +44 20 7123 4567 — the country switches with it.

value = "" · E.164

SPEC-02CURRENCYINPUT

Digits fill from the right, like a register.

value = 0 · minor units

SPEC-04PERCENTINPUT

Shows 12.5, emits 0.125.

value = 0 · fraction

Correct

The caret holds position mid-string. Paste normalises. Separators and zero-decimal currencies come from Intl, not from a hardcoded 100.

Native

Only shadcn CSS variables. Your palette, your radius, your focus ring, your dark mode — inherited, never overridden.

Yours

Installed through the registry: the source lands in your repo. Edit it, fork it, delete half of it. The headless hooks ship too.

Honest

Keyboard complete and axe-core in CI, with the four ARIA bugs it caught written up. Screen readers are not verified yet, and the audit says so.

01 — Components

Eleven hard inputs, one contract.

Every card below is a working input, not a screenshot. The readout under each one is the value onChange actually emits.

01PhoneInputstring

Paste +44 20 7123 4567 — the country switches with it.

value = "" · E.164

Digits fill from the right, like a register.

value = 0 · minor units

03MaskedInputstring

Click into the middle and type — the caret holds.

value = "" · raw, no mask characters

04PercentInputnumber

Shows 12.5, emits 0.125.

value = 0 · fraction

05CardInputstring

Try 4242… then 3782… — the grouping reflows to 4-6-5.

value = "" · digits only

90m, 1:30 and 1.5h all land on the same number.

value = 0 · seconds

07ColorInputstring

1.28:1below AA against #FFFFFF

Paste rgb(205, 242, 92) — it converts.

value = #CDF25C · hex

08CronInputstring

At 09:00, Monday through Friday

Next runsFri, Sep 18, 09:00 AMMon, Sep 21, 09:00 AMTue, Sep 22, 09:00 AM

Try */15 * * * * or @daily.

value = "0 9 * * 1-5"

1 MB and 1 MiB differ by 4.9%. Try both.

value = 0 · bytes

10IpInputstring

Try 10.0.0.0/24, or paste a whole log line.

value = ""

11MentionInputMentionValue

Type @ then a letter. Delete a mention and its id goes with it.

value = "" · ids []

02 — For agents

Most people get here through an agent.

So the library tells the agent what to do, rather than hoping it read the docs. Agents are not bad at finding components — they are bad at value semantics.

03 — Integration

Works with the form stack you already have.

And with none of it. The field detects whether something else owns its error state and gets out of the way.

import { useForm, Controller } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { moneySchema } from "@inputcn/currency/schema"
import { z } from "zod"

// The companion schema validates the CANONICAL value, so the resolver and the
// field can never disagree about what "valid" means.
const schema = z.object({ price: moneySchema({ positive: true, max: 100000 }) })

const { control } = useForm({ resolver: zodResolver(schema) })

<Controller
  name="price"
  control={control}
  render={({ field, fieldState }) => (
    <CurrencyInput label="Price" {...field} aria-invalid={fieldState.invalid} />
  )}
/>
// Or none of that. No schema, no resolver, no onBlur handler,
// no useState for the error message.
<CurrencyInput
  label="Budget"
  required
  positive
  max={100000}
  currency="GBP"
/>
04 — Form contract

One canonical value per component.

The display string is a view. This column is what you store, validate and send over the wire.

COMPONENTTEXAMPLE VALUE
PhoneInputstring"+442071234567"
CurrencyInputnumber129900
MaskedInputstring"12345678901"
PercentInputnumber0.125
CardInputstring"4242424242424242"
DurationInputnumber5400
ColorInputstring"#CDF25C"
CronInputstring"0 9 * * 1-5"
FileSizeInputnumber1048576
IpInputstring"10.0.0.0/24"
MentionInputMentionValue{ text, ids }
05 — Validation

Declare the rule as a prop.

Silent until the first blur, then live. The message is generated from the rule, and the rule name is on the element for your tests.

PROPCOMPONENTREJECTS
mobileOnlyPhoneInputLandline numbers, by prefix
positive maxCurrencyInputZero, negatives and anything over the cap
brands notExpiredCardInputCards outside the accepted set, and past expiries
multipleOfDurationInputOff-grid values — "a multiple of 15 minutes"
minIntervalCronInputSchedules that fire too often
minContrastColorInputUnreadable colours — as a warning, not an error
maxPrefix noPrivateIpInput0.0.0.0/0, and RFC 1918 in a public allowlist
maxMentionsMentionInputNotifying half the company

Full detail in Constraint props, and the complete generated list on each component page.