Input
A native input that renders on the server and ships 0 KB of JavaScript. The types refuse one with no name.
server - 0 KB
Input
server - 0 KBA native input that renders on the server and ships 0 KB of JavaScript. The types refuse one with no name.
Try it
size
invalid
fullWidth
The code
input.tsx
import { Input } from 'topclassui/input'
<div className="grid max-w-sm gap-1">
<label htmlFor="input-playground" className="text-sm font-medium text-body">Email</label>
<Input id="input-playground" name="email" type="email" autoComplete="email" />
</div>Outside a Field, Input needs id, aria-label or aria-labelledby: the types reject an unlabelled one.
Every state
6 statesImport from the package
topclassui/input
import { Input } from 'topclassui/input'Install once:npm i topclassui@beta zod
Examples
maxLengthhas a default per type: text 512, email 254, tel 32, url 2048, search and password 256. Pass your own number, ormaxLength={null}for no cap.- A number, a date or a checkbox takes no character cap: a number is bounded by min, max and step.
- It keeps its own width,
--tc-input-width, and less in a narrow row.fullWidthfills the container; beside a Button, give the Inputflex-1. size="custom"emits no size classes, so your own classes set the height, the padding and the type step.- A parent with
overflow-hiddenneeds 4px of padding, or it cuts the focus ring off.
labelled
We never share it
labelled.tsx
import { Input } from 'topclassui/input'
/** Outside a Field: a label points at the id, and aria-describedby ties the hint on. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-1">
<label htmlFor="demo-email" className="text-sm font-medium text-body">Email</label>
<Input
id="demo-email"
name="email"
type="email"
autoComplete="email"
required
fullWidth
aria-describedby="demo-email-hint"
/>
<span id="demo-email-hint" className="text-xs text-muted">We never share it</span>
</div>
)
}
types
types.tsx
import { Input } from 'topclassui/input'
/** Each text type takes its own default cap; a number or a date takes none at all. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-3">
<Input fullWidth type="email" autoComplete="email" placeholder="you@example.com" aria-label="Email" />
<Input fullWidth type="password" autoComplete="current-password" placeholder="Password" aria-label="Password" />
<Input fullWidth type="search" placeholder="Search" aria-label="Search" />
<Input fullWidth type="tel" autoComplete="tel" placeholder="+962 7 9000 0000" aria-label="Phone" />
<Input fullWidth type="url" placeholder="https://example.com" aria-label="Website" />
<Input fullWidth type="number" min={0} max={120} step={1} placeholder="Age" aria-label="Age" />
<Input fullWidth type="date" aria-label="Start date" />
<Input fullWidth maxLength={6} placeholder="Six characters" aria-label="Short code" />
<Input fullWidth maxLength={null} placeholder="No cap" aria-label="Uncapped" />
</div>
)
}
sizes
sizes.tsx
import { Input } from 'topclassui/input'
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
<Input size="xs" placeholder="Extra small" aria-label="Extra small" />
<Input size="sm" placeholder="Small" aria-label="Small" />
<Input placeholder="Medium" aria-label="Medium" />
<Input size="lg" placeholder="Large" aria-label="Large" />
<Input size="xl" placeholder="Extra large" aria-label="Extra large" />
</div>
)
}
full-width
full-width.tsx
import { Input } from 'topclassui/input'
export default function Demo() {
return (
<div className="grid max-w-md justify-items-start gap-3">
<Input fullWidth placeholder="Fills its container" aria-label="Full width" />
<Input placeholder="Keeps its own width" aria-label="Own width" />
</div>
)
}
row
row.tsx
import { Button } from 'topclassui/button'
import { Input } from 'topclassui/input'
/** A grid form sizes its track to the content, so only an input that can shrink fits a phone. */
export default function Demo() {
return (
<form className="grid gap-4">
<div data-row="own" className="flex gap-2">
<Input name="first" placeholder="First name" aria-label="First name" />
<Input name="last" placeholder="Last name" aria-label="Last name" />
</div>
<div data-row="grow" className="flex gap-2">
<Input name="city" className="flex-1" placeholder="City" aria-label="City" />
<Input name="postcode" className="flex-1" placeholder="Postcode" aria-label="Postcode" />
</div>
<div data-row="alone">
<Input name="phone" type="tel" placeholder="+962 7 9000 0000" aria-label="Phone" />
</div>
{/* flex-1 makes the field the one that gives way, so the Button keeps its whole label. */}
<div data-row="action" className="flex gap-2">
<Input name="invite" type="email" className="flex-1" placeholder="name@company.com" aria-label="Invite by email" />
<Button>Send invite</Button>
</div>
</form>
)
}
custom-size
custom-size.tsx
import { Input } from 'topclassui/input'
/** custom emits no height, padding or type step, so the caller's own classes set them. */
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
<Input placeholder="On the scale" aria-label="On the scale" />
<Input size="custom" className="h-14 px-5 text-lg" placeholder="Your own size" aria-label="Your own size" />
</div>
)
}
skeleton
skeleton.tsx
import { InputSkeleton } from 'topclassui/input'
/** Its own width at each size, as the field has, and the whole column with fullWidth. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-3">
<InputSkeleton size="sm" />
<InputSkeleton />
<InputSkeleton size="lg" />
<InputSkeleton fullWidth />
</div>
)
}
In Arabic, right to left
rtl.tsx
import { Input } from 'topclassui/input'
export default function Demo() {
return (
<div dir="rtl" lang="ar" className="grid max-w-sm gap-3">
<label htmlFor="demo-name-ar" className="text-sm font-medium text-body">الاسم</label>
<Input id="demo-name-ar" fullWidth placeholder="اكتب اسمك" />
<Input type="email" fullWidth defaultValue="omar@example.com" aria-label="البريد الإلكتروني" />
<Input type="number" fullWidth defaultValue="-1234.5" aria-label="الرصيد" />
<Input invalid fullWidth defaultValue="٠٧٩" aria-label="رقم الهاتف" />
</div>
)
}
Props
Keyboard and accessibility
- A name is required: a
<label htmlFor>pointing atid, oraria-label, oraria-labelledby. invalidsets aria-invalid, and the border turns to danger and thickens, so the error does not rest on colour alone.- On a touch screen the text is at least 16px, so iOS does not zoom the page on focus.
Right to left
The text starts at the right. Email, number, tel and url fields stay left to right, on the form's side.
Server and client
Renders on the server and ships no JavaScript.
Files: index.tsx.
On the chip: server - 0 KB.