My Duong
← Back to writing

A Button Is Never Just a Button

What a small control reveals about accessibility, interaction design, and frontend quality.

In a design file, a button can look simple.

A rectangle. A label. Maybe an icon. Maybe a few variants: primary, secondary, destructive, disabled, loading.

It is easy to look at that and think the decision is mostly visual.

What color is it?

How much padding does it have?

Is the corner radius right?

Does it match the design system?

Those things matter. A product should feel consistent. A button should look like it belongs in the interface.

But a button is not defined by how it looks.

A button is defined by the promise it makes.

When someone activates it, the product is saying: something will happen. Maybe a form will submit. Maybe a dialog will open. Maybe an item will be deleted. Maybe a menu will expand. Maybe the user’s settings will be saved.

That promise has to hold for more than a mouse click.

It has to hold for keyboard users. It has to hold for screen reader users. It has to hold while the action is loading. It has to hold when the action fails. It has to hold when the button is reused across twenty different product flows.

That is why a button is never just a button.

It is a small control, but it carries a lot of product behavior.

What a button is

At its simplest, a button is a control that triggers an action.

The WAI-ARIA Authoring Practices Guide describes a button as a widget that lets users trigger an action or event, such as submitting a form, opening a dialog, canceling an action, or deleting something.

MDN describes the HTML <button> element as an interactive element that can be activated by mouse, keyboard, touch, voice command, or assistive technology, and then performs an action such as submitting a form or opening a dialog.

That is the key idea: an action.

A button is usually for doing something in the current context.

Examples:

  • Save changes
  • Open settings
  • Submit application
  • Delete file
  • Add another address
  • Show filters
  • Close dialog

That seems obvious, but this is where teams often blur the line between what something looks like and what something is.

A button can look like a button.

A link can look like a button.

A menu item can look button-like.

A tab can look like a button.

A clickable card can contain a button-looking control.

The visual treatment is not the semantic decision.

The better question is:

What happens when the user activates this?

If the answer is “it performs an action,” you are probably dealing with a button.

One of the most useful early decisions is whether the control is a button or a link.

A link takes the user somewhere.

A button does something.

That distinction is not about visual style. It is about behavior.

Use a link when the control navigates to another page, route, document, or location:

<a href="/services">Explore services</a>

Use a button when the control performs an action:

<button type="button" onClick={openDialog}>
  Open settings
</button>

This can get confusing because teams often say things like:

Make that link look like a button.

That may be fine. A link can be styled to look like a button when the visual design calls for it.

But under the hood, it should still be a link if it navigates.

<a className="button button-primary" href="/contact">
  Start a conversation
</a>

That is okay because the control still behaves like a link. It has an href. It supports expected browser behavior. A user can open it in a new tab. Assistive technology can identify it as a link.

The reverse is where teams get into trouble:

<a href="#" onClick={saveChanges}>
  Save changes
</a>

That looks harmless, but it is mixing meanings. A link with a fake destination is being used as an action control.

The better version is:

<button type="button" onClick={saveChanges}>
  Save changes
</button>

The difference may feel small, but it affects keyboard behavior, screen reader expectations, browser behavior, testing, routing, and maintainability.

The element should match the job.

Native behavior is doing more than people think

A native button carries a lot of behavior before you add a single line of JavaScript.

  • It can receive focus.
  • It can be activated with expected keyboard commands.
  • It exposes a button role.
  • It can have an accessible name from its text.
  • It works with forms.
  • It participates in the accessibility tree in a way browsers and assistive technologies understand.

That does not mean native buttons are impossible to misuse. They still need good labels, visible focus styles, clear states, and thoughtful behavior.

But the native element gives you a better starting point.

This is fragile:

<div className="button" onClick={saveChanges}>
  Save changes
</div>

It may respond to a mouse click. It may look correct in a screenshot. But it is missing the built-in behavior that people expect from a button.

So teams often patch it:

<div
  className="button"
  role="button"
  tabIndex={0}
  onClick={saveChanges}
  onKeyDown={(event) => {
    if (event.key === 'Enter' || event.key === ' ') {
      saveChanges();
    }
  }}
>
  Save changes
</div>

Now it is longer. It is more fragile. It is easier to get wrong. And it still may not behave exactly like a real button.

The better version is still:

<button type="button" onClick={saveChanges}>
  Save changes
</button>

This is not about being purist. It is about using the platform where the platform already solves part of the problem.

WCAG’s Name, Role, Value criterion is about making sure user interface components have programmatically determinable names and roles, and that states and values can be set or communicated to assistive technologies. The W3C notes that standard HTML controls already meet this expectation when they are used according to specification.

That last part matters: when used according to specification.

Native elements are not magic. But they are usually the right foundation.

The anatomy of a button

A button is more than a rectangle and a label.

A real product button usually has several parts:

  • semantic element
  • accessible name
  • visual label
  • interaction behavior
  • keyboard behavior
  • focus style
  • state
  • feedback
  • placement
  • relationship to surrounding content

Some of those are visible. Some are not.

The visible parts are what teams tend to discuss first:

  • label
  • icon
  • size
  • color
  • spacing
  • variant
  • placement

The behavioral parts are where accessibility often lives:

  • Can it be reached with the keyboard?
  • Can it be activated with the keyboard?
  • Does it have a clear name?
  • Does it communicate state?
  • Does it make sense out of context?
  • What happens after activation?
  • Where does focus go?
  • What happens if the action fails?

A button is accessible when these parts work together.

A visually polished button with a weak label is still a problem.

<button type="button" onClick={deleteItem}>
  Click here
</button>

That label does not explain the action.

This is better:

<button type="button" onClick={deleteItem}>
  Delete file
</button>

If there are several repeated buttons, the name may need more context:

<button type="button" onClick={() => deleteFile(file.id)}>
  Delete quarterly-report.pdf
</button>

Or the visible label may stay short while the accessible name provides more context:

<button
  type="button"
  aria-label={`Delete ${file.name}`}
  onClick={() => deleteFile(file.id)}
>
  Delete
</button>

That kind of decision depends on the interface. The point is not that every button needs a long label. The point is that every button needs a name that communicates its action clearly enough.

Icon-only buttons need names

Icon-only buttons are common in modern interfaces.

A trash icon.

A pencil icon.

A gear icon.

A three-dot menu icon.

An X icon.

The problem is that an icon is not always a name.

This may look fine visually:

<button type="button" onClick={openSettings}>
  <SettingsIcon />
</button>

But depending on how the icon is implemented, the button may not have a useful accessible name.

A better version:

<button type="button" aria-label="Open settings" onClick={openSettings}>
  <SettingsIcon aria-hidden="true" />
</button>

Now the button has a name. The icon is treated as decorative because the accessible name is provided by the button.

For an icon-only button, the design review should not stop at:

Is the icon clear?

It should also ask:

What is the button called?

That question helps everyone. It helps screen reader users. It helps voice control users. It helps QA. It helps documentation. It helps the team clarify what the action actually is.

State is not just a visual variant

Buttons often have visual states:

  • default
  • hover
  • active
  • focus
  • disabled
  • loading
  • pressed
  • selected

Design tools can show these states. That is useful. But a visual state is not the same thing as an interaction state.

A loading button is a good example.

<button type="submit" disabled={isSaving}>
  {isSaving ? 'Saving…' : 'Save changes'}
</button>

That may be a reasonable start. But the product still needs to answer a few questions:

  • Is the button disabled while saving?
  • Is the form disabled too?
  • Does the label change?
  • Is there a spinner?
  • Is the spinner decorative?
  • What happens if saving fails?
  • Where is the error shown?
  • Is the user told that saving succeeded?
  • Can the user accidentally submit twice?

A design file might show a spinner inside the button. But the spinner is not the whole behavior.

The same is true for a destructive button:

<button type="button" className="button-danger" onClick={deleteAccount}>
  Delete account
</button>

The red color is only part of the pattern.

The real questions are:

  • Does this need confirmation?
  • Can the user undo it?
  • What exactly will be deleted?
  • Is the label specific enough?
  • What happens after deletion?
  • Where does focus move?

The more serious the action, the less the team should rely on color alone to carry meaning.

Disabled buttons can hide the problem

Disabled buttons are one of the places where product questions often get buried.

A disabled button can be useful. For example, it may prevent duplicate submissions while a request is already in progress:

<button type="submit" disabled={isSubmitting}>
  {isSubmitting ? 'Submitting…' : 'Submit'}
</button>

That can protect the system and reduce accidental double actions.

But disabled buttons can also create confusion.

Imagine a form with a disabled Submit button:

<button type="submit" disabled>
  Submit
</button>

The user sees that they cannot continue, but they may not know why.

Is a required field empty?

Is a format wrong?

Is something still loading?

Is the user not allowed to submit?

Is the form broken?

A disabled button often removes the moment where the product could explain the problem.

In many forms, it is better to let the user submit, then show clear validation messages tied to the fields that need attention.

<button type="submit">Submit</button>

Then, after submit:

<label htmlFor="email">Email</label>
<input id="email" name="email" aria-describedby="email-error" />
<p id="email-error">Enter an email address.</p>

That gives the user a path forward.

This does not mean “never disable buttons.” It means the disabled state should have a job.

A good review question is:

Is this disabled button preventing a real problem, or is it hiding instructions?

Focus is part of the button’s behavior

Focus is not decoration.

Focus is how many users know where they are and what they are about to operate.

WCAG Focus Visible requires that keyboard-operable interfaces have a mode where the keyboard focus indicator is visible. The W3C explains the purpose plainly: people need to know which element currently has keyboard focus.

For buttons, visible focus is the baseline.

button:focus-visible {
  outline: 3px solid currentColor;
  outline-offset: 3px;
}

The exact style can vary. The important thing is that it is visible, consistent, and not removed because someone disliked the browser default.

This is a common mistake:

button:focus {
  outline: none;
}

If you remove the default outline, you need to replace it with something at least as useful.

Focus also matters after the button is activated.

  • If a button opens a dialog, focus should move into the dialog.
  • If the dialog closes, focus should usually return to the button that opened it.
  • If a button removes an item from a list, focus should move somewhere sensible instead of disappearing.
  • If a form submit reveals errors, the user needs a clear way to find and fix those errors.

The WAI-ARIA Authoring Practices Guide has detailed keyboard guidance because custom widgets do not get all of this behavior for free. It notes that, unlike native HTML form elements, ARIA-based custom components require authors to provide keyboard support in code.

That is one reason native controls matter. The more custom the interaction, the more behavior the team owns.

Forms add another layer

Buttons inside forms have their own details.

One small but important habit is setting the button type.

<button type="button">Cancel</button>
<button type="submit">Save changes</button>

This avoids accidental form submission when a button is meant to perform a non-submit action.

MDN specifically notes that if buttons are not meant to submit form data, they should have type="button"; otherwise, they may try to submit the form.

This matters in real product code.

A form might have several buttons:

<form onSubmit={saveProfile}>
  <button type="button" onClick={addPhoneNumber}>
    Add phone number
  </button>

  <button type="button" onClick={cancelEditing}>
    Cancel
  </button>

  <button type="submit">
    Save profile
  </button>
</form>

Each button has a different job.

Only one submits the form.

That kind of clarity helps the browser, the user, the engineer, and the next person who has to maintain the code.

Toggle buttons are different from regular buttons

Some buttons perform an action once.

Other buttons represent an on/off state.

For example:

  • Mute
  • Bold
  • Favorite
  • Show grid
  • Pin sidebar

These are often toggle buttons.

A toggle button needs to communicate not only the action, but also the current state.

<button
  type="button"
  aria-pressed={isMuted}
  onClick={toggleMuted}
>
  Mute
</button>

The WAI-ARIA button pattern describes aria-pressed as the way to tell assistive technologies that a button is a toggle button. It also notes an important detail: the label on a toggle button should not change when the pressed state changes.

That means this is usually better:

<button
  type="button"
  aria-pressed={isMuted}
  onClick={toggleMuted}
>
  Mute
</button>

than switching the label back and forth:

<button type="button" onClick={toggleMuted}>
  {isMuted ? 'Unmute' : 'Mute'}
</button>

There are cases where changing the label can make sense, but the team should be intentional. A toggle button with a stable label and a clear pressed state is often easier for assistive technology users to understand.

Again, the point is not memorizing one rule. The point is understanding the contract.

Is this button doing an action?

Or is it communicating a state?

Those are different jobs.

Buttons become design-system decisions

One bad button is a bug.

A vague button pattern is a system problem.

This is why buttons are such a useful design-system topic. They are small enough to understand, but broad enough to reveal how a team makes decisions.

A strong button component should define more than color and size.

It should answer questions like:

  • When should this be a button?
  • When should it be a link?
  • What variants exist?
  • What does each variant mean?
  • When is a destructive button allowed?
  • How should loading work?
  • How should disabled work?
  • Are icon-only buttons allowed?
  • How are icon-only buttons named?
  • Does the component support submit, reset, and button types?
  • What focus style is required?
  • What examples are recommended?
  • What examples should teams avoid?
  • What tests protect the behavior?

A weak design system gives teams a component and leaves the hard decisions scattered across product work.

A stronger design system gives teams guidance.

For example:

<Button variant="primary" type="submit">
  Save changes
</Button>

That can be fine, but the component API should still respect native button behavior.

A design-system button should not make it hard to set type. It should not remove focus styles. It should not hide accessible names. It should not turn every control into a generic clickable wrapper.

The component should make the right thing easier.

That is what “accessible by default” really means. It does not mean nobody has to think. It means the system gives people better defaults and fewer traps.

A button review checklist

When reviewing a button, I would not start with a long compliance checklist.

I would start with practical questions.

  • What happens when this is activated?
  • Is this really a button, or should it be a link?
  • Is the native element being used?
  • Can it be reached with the keyboard?
  • Can it be activated with the keyboard?
  • Is focus visible?
  • Does the button have a clear name?
  • If it uses only an icon, where does the accessible name come from?
  • Does the visual label match the action?
  • Does the button communicate loading, disabled, or pressed state correctly?
  • What happens after the action succeeds?
  • What happens after it fails?
  • Where does focus go if the interface changes?
  • Is this pattern already defined in the design system?
  • If not, should it be?

Those questions are not just for accessibility specialists.

They are useful for designers, engineers, product managers, QA testers, and anyone reviewing a product flow.

A button is a small place where cross-functional work becomes visible.

Design decides what the user sees.

Engineering decides what the browser gets.

Accessibility depends on both.

Product quality depends on the whole thing holding together.

What this teaches us

Buttons are a good starting point because they are familiar.

Everyone has used one. Everyone has designed one. Everyone has built one.

But when you slow down and look closely, a button touches almost every part of accessible product engineering:

  • semantics
  • naming
  • keyboard access
  • focus
  • state
  • feedback
  • forms
  • error handling
  • design systems
  • component APIs
  • testing
  • documentation

That is why the small details matter.

Not because teams need to obsess over buttons forever. But because the way a team handles buttons often shows how the team handles product behavior in general.

Do they rely on visuals alone?

Do they use the platform well?

Do they document decisions?

Do they think about state and feedback?

Do they consider keyboard and assistive technology users as part of the normal product audience?

Do they make the right thing easier to repeat?

That is the real work.

Where this goes next

This article is the foundation.

From here, there are deeper questions worth exploring:

  • How do button expectations differ on the web, iOS, and Android?
  • What do current AI tools get right and wrong when generating buttons?
  • How should teams write button guidelines that both humans and AI tools can follow?
  • What should an accessibility report explain when it flags a button issue?

Those are separate topics. They deserve their own space.

For now, the important point is simpler:

A button is not finished because it matches the mockup.

It is finished when it keeps the promise it makes to the person using it.

So the next time a button comes up in review, do not only ask whether it looks right.

Ask what it does.

Ask what it is called.

Ask how it behaves.

Ask what happens next.

Ask whether the product keeps the interaction contract.

That is where a button stops being just a rectangle.

And that is where accessibility becomes product quality.

← Back to writing