Field
The standard form row: label, control, and helper or error text — wrap any input. It is also where the system answers a wrong value: the shake is armed by the transition into an error, never by the presence of one — a page that arrives with server-rendered errors already on it would otherwise jitter every invalid row at once on load, which reads as a rendering fault rather than as an answer to something you just did. Feedback has to stay attached to the action that caused it. Getting the same field wrong twice still re-arms it, because the error clears while you type, so the second failure is a real transition rather than a value that was already sitting there.
import { Field, Input, Button } from "@pestoui/core"Label, hint & error
Shown on the agent card.
That workspace is already taken.
Validation error
Submit while the field is empty, or with something that isn't an address. The row runs six decaying stops over 450ms — the decay is the whole difference between a shake and a vibrate — and the message underneath enters on its own clock: it starts 4px high, 4px out of focus and fully transparent, then resolves all three to zero. Fix the value and submit again to watch it pass. The hint that replaces the error enters the same way, because the message line is keyed on its text, so a swap mounts a fresh node — and the enter is a @starting-style transition, which only a fresh node can fire.
The shake is gated, not automatic
One error, reached two ways. Set it on a row that is already on screen and the row shakes, because you are watching a state change happen. Remount the row with the error already present — which is what a server-rendered form does — and it stays perfectly still, because nothing changed while you were looking. The gate is a flag raised only when the error differs from the previous render, then dropped again on animationend so the next failure restarts the keyframes instead of inheriting a class that is already applied.
Lowercase letters, numbers and dashes.
Focus animates colour, not geometry
The input carries its focus ring at rest — full width, zero alpha — so focusing it only moves the ring's colour. Nothing grows. The second field is the same input with that resting ring taken away: focus now has to inflate three pixels of new geometry, and even eased, it reads as the control changing size rather than lighting up. Colour, background, border and shadow all travel together on one 150ms curve, which is also why the invalid state crossfades out of focus instead of snapping.
The default. Focus moves the ring's colour and nothing else.
Resting ring removed. Focus inflates new geometry.
Props
Pesto UI · 117 documented components