Components and states: buttons and fields that behave the same
The anatomy of a button, three kinds in six states, eight states and what they show, a field with errors and real states to try — with live examples.
In short
A component in a design system is not one picture but a set of states: a button looks different when the cursor is over it, when it is focused from the keyboard, when it is pressed, when it is unavailable and while it is loading. A field has its own states: empty, filled, focused, with an error, confirmed, disabled. If the layout shows only the default state, the developer invents the rest — and every component ends up behaving differently. A complete component describes its anatomy, its variants and all its states with values from tokens, so it looks and behaves the same on every page.
The anatomy of a button
Before the states — the parts. Each part has a value from tokens, not a number chosen for this one button.
- Container: background color.accent-strong, height 48 px
- Side padding: space.5 = 24 px
- Label: font.step-0, weight 600
- Gap before the icon: space.2 = 8 px
- Icon: 20 px, the colour of the label
- Radius: radius.full — fully rounded ends
Three kinds × six states
The whole button on one sheet. This is what a layout must contain before development starts.
| Default | Hover | Focus | Pressed | Disabled | Loading | |
|---|---|---|---|---|---|---|
| Primary | Buy | Buy | Buy | Buy | Buy | Buy |
| Secondary | Buy | Buy | Buy | Buy | Buy | Buy |
| Ghost | Buy | Buy | Buy | Buy | Buy | Buy |
Eighteen cells instead of three buttons. The difference is not in the amount of drawing but in the number of questions the developer will not have to ask: what happens on hover, how the focus is shown, what a disabled ghost button looks like.
Eight states and what they must show
Each state answers a question of the person: can I press this, did it work, what is wrong.
| State | When | What changes |
|---|---|---|
| Default | the element is ready | the base look |
| Hover | the cursor is over it | a slightly different background |
| Focus | reached from the keyboard | a clearly visible ring |
| Pressed | at the moment of the click | a darker tone and a slight shift |
| Disabled | the action is not possible yet | a muted look, no reaction |
| Loading | the action is in progress | a spinner and a busy label |
| Error | the value is wrong | a signal colour and a hint how to fix |
| Success | the value is checked | a quiet confirmation |
A field in six states
The error does not only colour the border — it says what is wrong and how to fix it.
Try it: real states
Move the cursor, press Tab, click. In the field type an address without a domain and leave it — the error appears only after you have typed.
States in code: 2 examples
A button where every state takes its values from tokens, and markup for loading and errors that screen readers understand.
A button and its states
One line per state; no colour is written directly.
/* A button: every state is described, every value comes from tokens */
.button {
display: inline-flex;
align-items: center;
gap: var(--space-2);
min-height: 2.75rem; /* 44px: a comfortable target for a finger */
padding-inline: var(--space-5);
border: 0;
border-radius: var(--radius-full);
background: var(--color-accent-strong);
color: var(--color-on-accent-strong);
font: 600 var(--step-0) / 1 var(--font-text);
transition: background-color var(--duration-fast), transform var(--duration-fast);
}
.button:hover { background: var(--color-accent-strong-hover); }
.button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 3px; }
.button:active { transform: translateY(1px) scale(0.98); }
.button:disabled { background: var(--color-disabled); color: var(--color-text-muted); cursor: not-allowed; }
.button[aria-busy="true"] { cursor: progress; }
Loading and errors in markup
aria-busy and aria-describedby tell assistive technologies what the eye sees.
<!-- Loading: the button is busy, cannot be pressed twice, and says what is happening -->
<button class="button" type="submit" disabled aria-busy="true">
<span class="spinner" aria-hidden="true"></span>
Saving…
</button>
<!-- An error belongs to its field, and the field points to it -->
<label for="email">Email</label>
<input id="email" type="email" aria-invalid="true" aria-describedby="email-error">
<p id="email-error" class="field-error">Check the address: a domain is needed after @</p>
7 rules of a complete component
-
01
All states in the layout
A component without states is a sketch, not a component.
-
02
Focus is always visible
A ring that can be seen on any background — for those who use the keyboard.
-
03
Hover is not the only signal
On phones there is no cursor; the element must look clickable without it.
-
04
44 px for a finger
Clickable elements are at least 44 px high, even if they look smaller.
-
05
Errors say what to do
“Check the address: a domain is needed after @”, not “Invalid value”.
-
06
Not only colour
Errors and success also have text or an icon — for those who do not distinguish colours.
-
07
Values from tokens
Every state uses roles, so a new theme does not break any of them.
Common mistakes with components
-
Only the default state
Every developer invents hover and focus in their own way.
-
Removed focus outline
outline: none without a replacement makes the site unusable from the keyboard.
-
A disabled button without a reason
The person does not understand what to do to make it work.
-
Errors before typing
A form that is red before anything was typed scares people away.
-
No loading state
The person presses again and sends the order twice.
-
Copies instead of variants
Five slightly different buttons instead of one with three variants.
Questions about components and states
What states does a button have?
Default, hover, focus, pressed, disabled and loading; sometimes also selected.
What is the difference between :focus and :focus-visible?
:focus-visible shows the ring when the person uses the keyboard and does not show it after a mouse click.
When should errors appear in a form?
After the person has typed and left the field, or after an attempt to submit — :user-invalid does exactly that.
Should a disabled button be used at all?
Sometimes it is better to keep the button active and explain the problem on click.
What size should clickable elements be?
At least 44 × 44 px of clickable area, even if the visible element is smaller.
How many button variants does a system need?
Usually three: primary, secondary and ghost — plus a dangerous action where it is needed.
Online form
Components
without guesswork
I design and build component libraries where every state is described — in the layout and in the code. Tell me about the project — I answer within one working day.