Complete Button
Every state a real button has: hover, press, keyboard focus, loading at a locked width, done, disabled
Build my primary button properly. Not a new effect, just every state it should already have. The colours, the wording and the size are listed at the end; use those exact values. Render only the button itself. Do not add state names, captions, status text, a legend, or any label beside it. The button shows one label at a time and nothing else. The three labels are one verb in three tenses: Send / Sending / Sent, Save / Saving / Saved, Book / Booking / Booked. Keep them in the same voice. Sizing. If a width is given, use it. If not, put all three labels inside one CSS grid cell: a wrapper with display: grid, and every label span set to grid-area: 1 / 1. The grid then sizes itself to the longest label, spinner and checkmark included, and only the active label sits at opacity 1. The button is born at its final width and never resizes, with no JavaScript measuring anything. If instead you swap the text in and out, the button will shrink to the spinner and every element after it on the line will jump. At rest: the resting colour, a high-contrast label, a pill or a consistent radius, and one soft shadow around 0 2px 6px at 12% opacity. Semibold label, not all-caps unless the rest of the site is. Transitions: name the properties one by one. background-color 200ms, transform 140ms, box-shadow 200ms, with an ease-out curve like cubic-bezier(0.22, 1, 0.36, 1). Never use transition: all, because it drags layout properties into the animation and that single choice is most of what makes a hand-built button feel cheap. Transform is faster than colour on purpose, so the press feels immediate while the colour catches up. On hover: switch to the hover colour and lift the button 1px with translateY. Grow the shadow and soften it at the same time, to roughly 0 6px 16px at 18%. A shadow that only grows reads as a sticker; one that grows and blurs reads as distance. On press, via :active and pointerdown: scale to 0.975, drop the lift back to 0, tighten the shadow to around 0 1px 2px, and keep the hover colour. The button should feel pushed into the page, not reset. Most buttons skip this state entirely, which is why they feel like pictures of buttons. While it runs and after it finishes, the button keeps the hover colour for as long as the pointer is still on it. Tie the colour to real hover, not to the submitting state, or the button will drop back to its resting colour the instant it is clicked while the cursor has not moved. Keyboard focus: use :focus-visible, not :focus, so the ring appears for keyboard users and not on every mouse click. Draw it with a two-layer box-shadow, an inner ring in the page background colour then an outer ring in the hover colour, which keeps a visible gap on any background. Set outline: none only after that ring exists. While it runs: cross-fade to the spinner and the running label, and set aria-busy to true. When it finishes: cross-fade to a checkmark that draws itself, an SVG path animating stroke-dasharray from 0 to full over about 340ms, beside the finished label. Hold for roughly a second, then return to rest. When it is unavailable: drop opacity to about 0.38, remove the shadow, set cursor: not-allowed, and add the disabled attribute. Same size, same label. A button that renames itself to the word Disabled is a demo, not a product. Under prefers-reduced-motion: keep only a short background-color transition. Drop the lift, the scale, and the checkmark draw. Every state must still be distinguishable by colour and content alone. Make it mine: - Label the button "Send message" at rest. - Label it "Sending" while it runs, beside the spinner. - Label it "Sent" when it finishes, beside the checkmark. Build the component, not this demo of it. - Ambient motion that IS the component may loop: a drifting background, a marquee, a spinner while something is genuinely loading. - Do not add any timer, interval, loop or script that performs a person's action by itself. No auto-hover, no auto-click, no cycling through states, no pretend submitting, no replaying the interaction on a schedule. - Every state that answers a person waits for a real event from that person. - Placeholder copy and sample content are welcome. Simulated behaviour is not.