One form, seven components
An expense claim, built the way you would build it: seven custom elements inside a plain <form>, no framework and no wiring. Fill it in and submit — the readout underneath is the real FormData the server would receive.
Submit the form to see exactly what your server would receive.Submit it empty and every field speaks up at once — including the ones you never visited, because a submitted form has asked for everything wrong with it. Reset puts all seven back where they started.
Nothing here is intercepted, adapted or bridged. Each element is form-associated through ElementInternals, so the browser collects the values, runs the constraints and fires reset exactly as it does for <input> — which is why the date submits an ISO string, the amount submits its raw digits, and the receipt arrives as a file under its own name. A React form library would receive the same events.
Input
The first of the form family. A real <input> in a shadow root that still takes part in its form, with one message at a time underneath it.
01011990, not 01/01/1990 — submit-value decides, and defaults to the raw value.
A required field is invalid from the moment it renders, and says nothing about it until you have left it. Once it has complained, it re-checks on every keystroke rather than making you tab away to find out whether you have fixed it.
Textarea
The same chrome as the input, and the one thing every application ends up writing itself: a box that grows with the text in it.
No mask and no prefix or suffix slots, deliberately: caret arithmetic has no meaning across line breaks, and an icon pinned beside a block of text has nothing to line up with.
Select
One choice or several, grouped or flat, and windowed — so a list of five thousand costs about twenty elements. The options are data rather than markup, which is what makes that possible.
The list is in the browser's top layer, so it escapes any overflow: hidden above it and needs no z-index. It caps its own height against the room actually on the page, flips above the control when there is more space there, and follows the control if a scrolling panel moves it.
Combobox
A text field that filters the same list. A separate element from the select rather than a flag on it — the trigger is an input, so every printable key is text and Home and End belong to the caret.
allow-custom is off, so text that matches nothing is discarded when you leave the field rather than becoming a value nobody chose.
server-filter the results would be filtered a second time by the same query — which works for a substring search and quietly discards everything a fuzzy one was for.
The result count is announced through a live region, because a list that silently shrinks as you type tells a sighted user everything and a screen reader user nothing. Only when the count changes: a live region re-reading the same sentence on every keystroke is worse than saying nothing.
Date picker
A date typed into a field or chosen from a calendar. The first component here that is a grid rather than a list — and the one where the locale decides more than the month names.
09/05/2026 is the 9th of May in London and the 5th of September in New York, and both are real dates — so reading it wrong does not throw, it books the wrong day. The order comes from Intl, and the mask, the placeholder and the parser are all built from it.
<input type="time"> — it brings its own keyboard, its own locale formatting and its own spinner, none of which a custom control would do better. The slider could not do that because there is no native two-ended slider; there is a native time field.
Arrow keys move a day, up and down a week, Page Up and Down a month, and with Shift a year — which is the only way to reach a date of birth without forty clicks. The grid has one tab stop, not forty-two. And not one Date object crosses into the component: new Date('2026-03-29') is the 28th in Honolulu, and adding a day across a spring-forward lands on the day it started.
Upload
The last of the fields, and the one where choosing the file is the easy half. Everything worth having is what comes after: a row per file, a bar per file, and a way to retry the one that failed without starting the other thirty-nine again.
url nor uploader and the field simply holds the files, submitting one FormData entry each under its own name — exactly what <input type="file" multiple> does, so a handler already written for one needs no change.
Dragging is never the only way to do anything — the button opens the platform's own picker, which is how most people will use it regardless. The highlight is driven by a depth count, because dragenter and dragleave fire for every descendant and the naive version flickers the moment the pointer crosses a child. A dropped folder is refused by name rather than uploaded: it arrives as a zero-byte, typeless file, and sending it writes an empty file onto the server with a convincing name on it.