|$ curl https://forge-ai.dev/api/markdown?path=docs/html/buttons
$cat docs/html-buttons.md
updated Today·32 min read·published

HTML Buttons

HTMLFormsAccessibilityIntermediate🎯Free Tools
Introduction

The <button> element is the primary control for triggering actions that do not navigate to a new URL. It submits forms, opens dialogs, toggles UI state, runs scripts, and resets fields. Getting buttons right means choosing the correct element, setting an explicit type, wiring form association attributes when needed, and exposing an accessible name and state.

This guide covers the full surface area of HTML buttons: how they differ from links and <input type="button">, the three type values, all form* override attributes, disabled and autofocus behavior, icon-only patterns, toggle buttons with aria-pressed, dialogs, CSS reset pitfalls, keyboard activation rules, and browser quirks that still surprise teams in production.

Prefer native <button> over role-button divs. Prefer links for navigation. Always declare type inside forms — the default is submit, which causes many accidental submissions.

button vs a vs input[type=button]

Three elements are commonly confused: <button>, <a href>, and <input type="button">. They look similar when styled, but they differ in semantics, keyboard behavior, form participation, and content models.

ElementPrimary jobContentForm role
<button>Perform an actionPhrasing content (icons + text)submit / reset / button
<a href>Navigate / download / jumpPhrasing / flow (flexible)None — not a form control
<input type="button">Action (legacy/simple)value attribute only (no children)button (not submit unless type=submit)
<input type="submit">Submit associated formvalue as labelsubmit
<div role="button">Anti-pattern fallbackAnything (you own a11y)None
choose-element.html
HTML
1<!-- Navigation: use a link -->
2<a href="/docs/html/forms">Open forms guide</a>
3
4<!-- Action: use a button -->
5<button type="button" id="open-filters">Open filters</button>
6
7<!-- Void input button — label comes from value only -->
8<input type="button" value="Print" onclick="window.print()" />
9
10<!-- Never: fake button that should be a link -->
11<button type="button" onclick="location.href='/pricing'">Pricing</button>
12
13<!-- Never: fake link that should be a button -->
14<a href="#" onclick="save(); return false;">Save</a>

best practice

If activating the control takes the user to a new URL (or fragment), use a link. If it changes state on the current page or submits data, use a button. Do not decide based on visual style alone.
preview
type: submit, reset, and button

The type attribute on <button> accepts three values. Omitting it defaults to submit — even for buttons that only run JavaScript. That default is the root cause of many “page reloads on click” bugs.

typeDefault actionWhen to use
submitSubmit the associated formPrimary form save / continue
resetReset fields to initial valuesRare — confirm before use
buttonNone (script / UI only)Almost all JS-driven controls
button-types.html
HTML
1<form method="post" action="/api/signup">
2 <label>Email <input name="email" type="email" required /></label>
3
4 <!-- Explicit submit -->
5 <button type="submit">Create account</button>
6
7 <!-- Explicit non-submitting control -->
8 <button type="button" id="toggle-password">Show password</button>
9
10 <!-- Reset restores defaultValue / defaultChecked -->
11 <button type="reset">Clear form</button>
12</form>
13
14<!-- Outside a form, type still matters for consistency -->
15<button type="button">Open menu</button>

warning

A <button> inside a form with no type attribute submits the form. Always write type="button" for menu toggles, tabs, and icon controls nested in forms.
📝

note

type="reset" is destructive and usually poor UX. Prefer a “Clear” control that confirms, or clears only specific fields via script.
form and form* override attributes

HTML associates a button with a form either by nesting or by the form attribute pointing at a form id. Override attributes on the button can change how that specific submit control posts data — without changing the form element itself.

AttributeOverridesExample
formWhich form the control belongs toform="checkout"
formactionform action URLformaction="/api/draft"
formenctypeenctypeformenctype="multipart/form-data"
formmethodmethodformmethod="get"
formnovalidateSkips constraint validationformnovalidate
formtargetBrowsing context / targetformtarget="_blank"
form-overrides.html
HTML
1<form id="profile" method="post" action="/api/profile" enctype="application/x-www-form-urlencoded">
2 <label>Name <input name="name" required /></label>
3 <label>Avatar <input name="avatar" type="file" /></label>
4
5 <!-- Nested submit uses the form's action/method -->
6 <button type="submit">Save profile</button>
7</form>
8
9<!-- Button outside the form, associated by id -->
10<button type="submit" form="profile">Save (footer)</button>
11
12<!-- Same form, different endpoint + skip validation (draft) -->
13<button
14 type="submit"
15 form="profile"
16 formaction="/api/profile/draft"
17 formmethod="post"
18 formnovalidate
19>
20 Save draft
21</button>
22
23<!-- Upload path needs multipart -->
24<button
25 type="submit"
26 form="profile"
27 formaction="/api/profile/avatar"
28 formenctype="multipart/form-data"
29>
30 Upload avatar only
31</button>
32
33<!-- Open printable confirmation in a new tab -->
34<button type="submit" form="profile" formaction="/profile/print" formmethod="get" formtarget="_blank">
35 Print preview
36</button>

info

formnovalidate on a submit button skips HTML constraint validation for that submission only. Use it for “Save draft” flows; keep full validation on the final submit.
🔥

pro tip

The form attribute lets you place submit controls in sticky footers or dialogs while the fields live elsewhere in the DOM — useful for complex layouts without wrapping the whole page in one form.
disabled, autofocus, name, and value

Boolean and name/value attributes control focus, participation in form data, and whether the control can be activated.

AttributeEffect
disabledNot focusable, not successful, not activatable; matches :disabled
autofocusFocus on page load or when a dialog opens (use sparingly)
nameIf present on a successful submit button, included in form data
valueValue paired with name when the button is the submitter
name-value-disabled.html
HTML
1<form method="post" action="/checkout">
2 <button type="submit" name="intent" value="pay">Pay now</button>
3 <button type="submit" name="intent" value="save-cart">Save cart</button>
4</form>
5
6<!-- Only the activated submit control is successful -->
7<!-- POST body might be: intent=pay -->
8
9<button type="button" disabled aria-busy="true">Saving…</button>
10
11<dialog id="confirm">
12 <form method="dialog">
13 <p>Delete this item?</p>
14 <button type="submit" value="cancel">Cancel</button>
15 <button type="submit" value="confirm" autofocus>Delete</button>
16 </form>
17</dialog>
disable-during-async.js
JavaScript
1const pay = document.querySelector('button[value="pay"]');
2pay.disabled = true; // prevent double submit
3pay.setAttribute('aria-busy', 'true');
4
5// Re-enable after fetch settles
6async function onPay(e) {
7 e.preventDefault();
8 pay.disabled = true;
9 try {
10 await fetch('/checkout', { method: 'POST', body: new FormData(e.target) });
11 } finally {
12 pay.disabled = false;
13 pay.removeAttribute('aria-busy');
14 }
15}

warning

Disabled buttons are removed from the accessibility tree as operable controls in many browsers. Prefer aria-disabled="true" plus preventing activation in script when you still need the control to remain focusable and explainable.
📝

note

Only one submitter is successful: the button that was activated (or the implicit default submit). Its name=value pair is what the server sees for multi-intent forms.
Icons and accessible names

Screen readers announce a button by its accessible name. Visible text usually provides that name. Icon-only buttons must get a name from aria-label, aria-labelledby, or visually hidden text — not from a title tooltip alone.

icon-buttons.html
HTML
1<!-- Good: visible text -->
2<button type="button">Search</button>
3
4<!-- Good: icon + visually hidden text -->
5<button type="button">
6 <svg aria-hidden="true" focusable="false" width="16" height="16">...</svg>
7 <span class="visually-hidden">Search</span>
8</button>
9
10<!-- Good: aria-label when no visible text -->
11<button type="button" aria-label="Search">
12 <svg aria-hidden="true" focusable="false" width="16" height="16">...</svg>
13</button>
14
15<!-- Bad: decorative SVG becomes the name -->
16<button type="button">
17 <svg><!-- paths with no title --></svg>
18</button>
19
20<!-- Bad: title alone is unreliable -->
21<button type="button" title="Search">
22 <svg aria-hidden="true"></svg>
23</button>
visually-hidden.css
CSS
1.visually-hidden {
2 position: absolute;
3 width: 1px;
4 height: 1px;
5 padding: 0;
6 margin: -1px;
7 overflow: hidden;
8 clip: rect(0, 0, 0, 0);
9 white-space: nowrap;
10 border: 0;
11}
preview

best practice

Mark decorative icons with aria-hidden="true" and focusable="false" so they do not steal the accessible name or create extra tab stops in older SVG focus models.
Toggle buttons: aria-pressed vs native

There is no native HTML “pressed toggle button” element. For on/off toolbar buttons, use <button type="button"> with aria-pressed="true|false". For binary settings that look like switches, prefer a checkbox (optionally styled) — it has built-in checked state and form participation.

PatternElement / APIUse when
Toggle buttonbutton + aria-pressedFormatting toolbars, mute, bold
Switch / settinginput type=checkbox (+ role=switch optional)Preferences that submit as data
Exclusive optionsradiogroup / segmented radiosOne of many views
Expand/collapsearia-expanded on buttonMenus, disclosures, accordions
toggles.html
HTML
1<!-- Toggle button (pressed state) -->
2<button
3 type="button"
4 id="bold"
5 aria-pressed="false"
6 aria-label="Bold"
7>
8 B
9</button>
10
11<!-- Native checkbox as switch (better for settings) -->
12<label>
13 <input type="checkbox" role="switch" name="notifications" />
14 Desktop notifications
15</label>
16
17<!-- Expand/collapse is aria-expanded, not aria-pressed -->
18<button type="button" aria-expanded="false" aria-controls="menu">
19 Account
20</button>
21<ul id="menu" hidden>...</ul>
aria-pressed.js
JavaScript
1const bold = document.getElementById('bold');
2bold.addEventListener('click', () => {
3 const next = bold.getAttribute('aria-pressed') !== 'true';
4 bold.setAttribute('aria-pressed', String(next));
5 document.execCommand?.('bold'); // legacy example only
6});
preview
📝

note

Do not mix aria-pressed with aria-expanded on the same control for different meanings. Pick the state that matches the UI pattern.
Buttons in dialogs

Inside <dialog> or the Popover API, buttons close UI, confirm destructive actions, or submit nested forms. Prefer method="dialog" forms for lightweight confirm/cancel without custom close wiring.

dialog-buttons.html
HTML
1<button type="button" id="open">Delete file…</button>
2
3<dialog id="del">
4 <form method="dialog">
5 <h2>Delete file?</h2>
6 <p>This cannot be undone.</p>
7 <button type="submit" value="cancel">Cancel</button>
8 <button type="submit" value="confirm">Delete</button>
9 </form>
10</dialog>
11
12<script>
13 const dialog = document.getElementById('del');
14 document.getElementById('open').onclick = () => dialog.showModal();
15 dialog.addEventListener('close', () => {
16 if (dialog.returnValue === 'confirm') {
17 // perform delete
18 }
19 });
20</script>

best practice

In modal dialogs, put the safest action first in DOM order for reading order, and ensure focus moves into the dialog on open and restores on close. Native showModal() handles the focus trap; custom modals often get this wrong.

info

A button with formmethod="dialog" can close a dialog form even if the form is not method=dialog — useful for mixed forms.
Styling and reset pitfalls

Browser default button styles differ wildly. Resets often strip focus outlines, set cursor incorrectly, or make buttons inherit font sizes inconsistently. Restore focus visibility and do not remove :focus-visible styles.

button-css.css
CSS
1/* Dangerous reset */
2button {
3 all: unset; /* removes focus ring, keyboard affordances perception */
4}
5
6/* Safer baseline */
7button {
8 font: inherit;
9 color: inherit;
10 background: #111;
11 border: 1px solid #00FF41;
12 padding: 0.5rem 1rem;
13 border-radius: 4px;
14 cursor: pointer;
15}
16
17button:disabled {
18 opacity: 0.5;
19 cursor: not-allowed;
20}
21
22button:focus-visible {
23 outline: 2px solid #00FF41;
24 outline-offset: 2px;
25}
26
27/* Do not do this */
28button:focus {
29 outline: none; /* kills keyboard UX unless replaced */
30}

danger

Removing outlines without a :focus-visible replacement fails WCAG 2.4.7 Focus Visible. Keyboard users cannot tell which control is active.

warning

User-agent stylesheets treat <button> and <input type=button> differently. Test both if your design system still ships input buttons.
Keyboard activation

Native buttons activate on Space and Enter (with slight press/release nuances). Links activate on Enter only. Custom role=button elements must reimplement Space handling, prevent page scroll on Space, and manage tabindex — another reason to prefer real buttons.

keyboard.js
JavaScript
1// Native button: no extra keyboard code needed
2el.addEventListener('click', onAction);
3
4// If you must polyfill role=button (avoid):
5div.addEventListener('keydown', (e) => {
6 if (e.key === ' ' || e.key === 'Enter') {
7 e.preventDefault();
8 div.click();
9 }
10});
ControlEnterSpaceTab
buttonActivatesActivatesFocuses
a[href]ActivatesScrolls pageFocuses
div role=buttonYou must handleYou must handleNeeds tabindex=0

best practice

Rely on the click event for activation. It fires for pointer and for synthesized keyboard activation on native buttons. Do not require separate keyup logic for real buttons.
Browser quirks

A few long-lived quirks still affect production forms and component libraries.

QuirkDetailMitigation
Default type=submitOmitted type submits enclosing formAlways set type
Implicit submissionEnter in a text field submitsSingle text field forms submit unexpectedly
IE legacy typeHistorical bugs around button typeIrrelevant in modern evergreen, still in old docs
iOS tap delay (old)300ms myths lingerUse touch-action / modern browsers
Disabled pointer eventsClicks ignored; tooltips hardWrap or use aria-disabled pattern
formaction + GETQuery serialization quirks with filesDo not GET file inputs
quirks.html
HTML
1<!-- Implicit submission: one text field + Enter submits -->
2<form action="/search">
3 <input type="search" name="q" />
4 <!-- implicit submit even without a visible button in some cases -->
5</form>
6
7<!-- Add type=button siblings so they do not submit -->
8<form>
9 <input name="q" />
10 <button type="button" id="clear">Clear</button>
11 <button type="submit">Search</button>
12</form>
📝

note

Successful controls and the submitter algorithm are defined in the HTML Standard. When debugging missing POST fields, check which button was the submitter and whether it had name/value.
Best practices checklist

Use this checklist when reviewing button markup in PRs and design systems.

untitled.text
TEXT
1[ ] Correct element: button for actions, a for navigation
2[ ] Explicit type on every button (especially inside forms)
3[ ] Accessible name from text, aria-label, or aria-labelledby
4[ ] Decorative icons are aria-hidden
5[ ] Loading state disables duplicate submits (or aria-disabled)
6[ ] Focus-visible styles preserved after CSS resets
7[ ] Toggle state uses aria-pressed; menus use aria-expanded
8[ ] Destructive actions confirmed in a dialog
9[ ] form* overrides documented for multi-submit forms
10[ ] No href="#" / javascript: void “buttons”
11[ ] Hit target ≥ 24×24 CSS px (prefer 44×44 on touch)
12[ ] Contrast meets WCAG for text and UI components

best practice

Design systems should export Button as a thin wrapper around the native element, not a div. Forward type, disabled, name, value, and form attributes unchanged.
$Blueprint — Engineering Documentation·Section ID: HTML-BUTTONS·Revision: 2.0

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.