Textarea
A native textarea rendered on the server. Autosize and the live counter are the only parts that load in the browser.
split - size not measured yet
Textarea
split - size not measured yetA native textarea rendered on the server. Autosize and the live counter are the only parts that load in the browser.
Try it
size
invalid
fullWidth
rows
maxRows
autosize
showCount
The code
textarea.tsx
import { Textarea } from 'topclassui/textarea'
<div className="grid max-w-sm gap-1">
<label htmlFor="textarea-playground" className="text-sm font-medium text-body">About you</label>
<Textarea id="textarea-playground" name="bio" placeholder="Write a short paragraph" />
</div>On its own it is server HTML; autosize or showCount is what loads the small client half.
Every state
6 statesImport from the package
topclassui/textarea
import { Textarea } from 'topclassui/textarea'Install once:npm i topclassui@beta zod
Examples
maxLengthis 5000 by default. Pass your own number, ormaxLength={null}for no cap; the browser stops typing and pasting there.- With
autosizethe rows follow the text fromrowsup tomaxRows, then it scrolls.maxRowsalone grows nothing. - The counter reads 42 / 500 under the field: always with
showCount, and withautosizealone from nine tenths of the cap. At the cap it turns bold; that is not an error. - With
autosizeorshowCountthe width belongs to a box around the textarea: usefullWidth, or size the parent. - With no script, what is typed is kept and sent, and a form reset is native.
basic
A short paragraph is enough
basic.tsx
import { Textarea } from 'topclassui/textarea'
/** A label points at the id, and aria-describedby ties the hint on. No script is needed for any of it. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-1">
<label htmlFor="textarea-basic-bio" className="text-sm font-medium text-body">About you</label>
<Textarea
id="textarea-basic-bio"
name="bio"
fullWidth
placeholder="Write a short paragraph"
aria-describedby="textarea-basic-hint"
/>
<span id="textarea-basic-hint" className="text-xs text-muted">A short paragraph is enough</span>
</div>
)
}
autosize
autosize.tsx
import { Textarea } from 'topclassui/textarea'
const LONG = ['One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight'].join('\n')
/** One empty field that grows from three rows to six as lines come, and one already past six that scrolls. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-3">
<Textarea fullWidth autosize placeholder="Grows as you write" aria-label="Grows" />
<Textarea fullWidth autosize defaultValue={LONG} aria-label="Past six rows" />
</div>
)
}
show-count
12 of 60 characters
12 of 12 characters
show-count.tsx
import { Textarea } from 'topclassui/textarea'
/** The count against the cap: always with showCount, from nine tenths of it with autosize alone, never with no cap. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-3">
<Textarea fullWidth showCount maxLength={60} defaultValue="Twelve chars" aria-label="Always counted" />
<Textarea fullWidth autosize maxLength={60} aria-label="Counted near the cap" />
<Textarea fullWidth showCount maxLength={12} defaultValue="Twelve chars" aria-label="At the cap" />
<Textarea fullWidth showCount maxLength={null} aria-label="No cap" />
</div>
)
}
sizes
sizes.tsx
import { Textarea } from 'topclassui/textarea'
/** The five steps of the field scale: the padding and the type step change, the three rows stay. */
export default function Demo() {
return (
<div className="grid justify-items-start gap-3">
<Textarea size="xs" placeholder="Extra small" aria-label="Extra small" />
<Textarea size="sm" placeholder="Small" aria-label="Small" />
<Textarea placeholder="Medium" aria-label="Medium" />
<Textarea size="lg" placeholder="Large" aria-label="Large" />
<Textarea size="xl" placeholder="Extra large" aria-label="Extra large" />
</div>
)
}
form
form.tsx
import { Button } from 'topclassui/button'
import { Textarea } from 'topclassui/textarea'
/** A plain form: the textarea sends what is typed before any script runs, and a reset needs none either. */
export default function Demo() {
return (
<form className="grid max-w-sm justify-items-start gap-3">
<label htmlFor="textarea-form-message" className="text-sm font-medium text-body">Message</label>
<Textarea id="textarea-form-message" name="message" fullWidth defaultValue="Hello" required />
<label htmlFor="textarea-form-notes" className="text-sm font-medium text-body">Notes</label>
<Textarea id="textarea-form-notes" name="notes" fullWidth autosize showCount />
<div className="flex flex-wrap gap-2">
<Button type="submit">Send</Button>
<Button type="reset" variant="outline">Clear</Button>
</div>
</form>
)
}
row
0 of 5,000 characters
0 of 5,000 characters
row.tsx
import { Textarea } from 'topclassui/textarea'
/** Two in a flex row: each keeps an Input's width while there is room, and both give way on a phone. */
export default function Demo() {
return (
<div className="grid gap-4">
<div data-row="plain" className="flex gap-2">
<Textarea name="pros" placeholder="What went well" aria-label="What went well" />
<Textarea name="cons" placeholder="What did not" aria-label="What did not" />
</div>
<div data-row="held" className="flex gap-2">
<Textarea name="pros-held" autosize showCount placeholder="What went well" aria-label="What went well, counted" />
<Textarea name="cons-held" autosize showCount placeholder="What did not" aria-label="What did not, counted" />
</div>
</div>
)
}
long-content
long-content.tsx
import { Textarea } from 'topclassui/textarea'
const WORD = 'Supercalifragilistic'.repeat(15)
const LINES = Array.from({ length: 60 }, (_, line) => `Line ${String(line + 1)}`).join('\n')
/** Hostile text: one word of 300 characters wraps inside the box, and sixty lines scroll inside it. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-3">
<Textarea fullWidth defaultValue={WORD} aria-label="One long word" />
<Textarea defaultValue={WORD} aria-label="One long word, own width" />
<Textarea fullWidth autosize defaultValue={LINES} aria-label="Sixty lines" />
</div>
)
}
skeleton
skeleton.tsx
import { Textarea, TextareaSkeleton } from 'topclassui/textarea'
const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const
/** Each field above the skeleton that holds its place, in a wrapper each: the same box. */
export default function Demo() {
return (
<div className="grid gap-4">
{SIZES.map((size) => (
<div key={size} data-pair={size} className="grid gap-2">
<div><Textarea size={size} placeholder="Message" aria-label={`Message, ${size}`} /></div>
<div><TextareaSkeleton size={size} /></div>
</div>
))}
<div data-pair="one-row" className="grid gap-2">
<div><Textarea size="xs" rows={1} placeholder="One row" aria-label="One row" /></div>
<div><TextareaSkeleton size="xs" rows={1} /></div>
</div>
<div data-pair="full" className="grid gap-2">
<div><Textarea fullWidth rows={5} placeholder="Fills its container" aria-label="Full width" /></div>
<div><TextareaSkeleton fullWidth rows={5} /></div>
</div>
</div>
)
}
In Arabic, right to left
عدد الأحرف: ٢٤ من ٥٠٠
rtl.tsx
import { TopClassProvider } from 'topclassui/provider'
import { Textarea } from 'topclassui/textarea'
/** Arabic, inside TopClassProvider as the root layout would mount it, with Arabic-Indic digits: the
* counter sits at the inline end, on the left, and its two numbers keep their order. */
export default function Demo() {
return (
<TopClassProvider direction="rtl" locale="ar" numberingSystem="arab">
<div dir="rtl" lang="ar" className="grid max-w-sm gap-3">
<label htmlFor="textarea-rtl-bio" className="text-sm font-medium text-body">نبذة عنك</label>
<Textarea id="textarea-rtl-bio" name="bio" fullWidth showCount maxLength={500} defaultValue="مرحبا بكم في TopClass UI" />
<Textarea name="note" fullWidth dir="auto" placeholder="اكتب ملاحظتك" aria-label="ملاحظة" />
</div>
</TopClassProvider>
)
}
Props
Keyboard and accessibility
- A name is required: a
<label htmlFor>pointing atid, oraria-label, oraria-labelledby. - The counter is the field's description, so a screen reader reads it after the name. It is not a live region.
invalidsets aria-invalid, and the border turns to danger and thickens.
Right to left
The text starts at the right and the counter sits at the inline end, on the left. Set the direction to auto to let each text follow its first letter.
Server and client
Renders on the server; the interactive part is a client component that ships only when it is used.
Files: index.tsx, client.tsx.
On the chip: split - size not measured yet.