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.

Design systems Updated

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.

Place the order 123 456
  1. Container: background color.accent-strong, height 48 px
  2. Side padding: space.5 = 24 px
  3. Label: font.step-0, weight 600
  4. Gap before the icon: space.2 = 8 px
  5. Icon: 20 px, the colour of the label
  6. 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.

DefaultHoverFocusPressedDisabledLoading
Primary BuyBuyBuyBuyBuyBuy
Secondary BuyBuyBuyBuyBuyBuy
Ghost BuyBuyBuyBuyBuyBuy

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.

StateWhenWhat 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.

Default Email [email protected] We will send the order confirmation
Filled Email [email protected] We will send the order confirmation
Focus Email anna@exa We will send the order confirmation
Error Email anna@ Check the address: a domain is needed after @
Valid Email [email protected] Address confirmed
Disabled Email [email protected] The email is changed in the profile

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.

Hover, tab to it, press it
Check the email address

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.

button.css
/* 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.

states.html
<!-- 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

  1. 01

    All states in the layout

    A component without states is a sketch, not a component.

  2. 02

    Focus is always visible

    A ring that can be seen on any background — for those who use the keyboard.

  3. 03

    Hover is not the only signal

    On phones there is no cursor; the element must look clickable without it.

  4. 04

    44 px for a finger

    Clickable elements are at least 44 px high, even if they look smaller.

  5. 05

    Errors say what to do

    “Check the address: a domain is needed after @”, not “Invalid value”.

  6. 06

    Not only colour

    Errors and success also have text or an icon — for those who do not distinguish colours.

  7. 07

    Values from tokens

    Every state uses roles, so a new theme does not break any of them.

Common mistakes with components

  1. Only the default state

    Every developer invents hover and focus in their own way.

  2. Removed focus outline

    outline: none without a replacement makes the site unusable from the keyboard.

  3. A disabled button without a reason

    The person does not understand what to do to make it work.

  4. Errors before typing

    A form that is red before anything was typed scares people away.

  5. No loading state

    The person presses again and sends the order twice.

  6. 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.

Or write to [email protected]