The Button That Ate Your Sprint
You've heard it a thousand times: "Use native HTML elements." But why? Madcampos' article "If you want to create a button from scratch, you must first create the universe" answers that question with a brutal, line-by-line walkthrough. The result: a custom button needs at least 15 distinct behaviors to match the native ``.
The Requirements List
The article references MDN, ARIA Authoring Practices Guide, and WCAG 2.2 to compile this non-exhaustive list:
- Role of button (SC 4.1.2)
- Accessible label (SC 4.1.2, SC 1.3.1)
- Focusable (SC 2.4.3, SC 2.4.7)
- Mouse click activation
- Touch activation
- Stylus/pointer activation
- Space and Enter key activation (SC 2.1.1)
- Type attribute (submit, reset, button)
- Name/value and form attributes
- Form validation participation (SC 3.3.1)
- Disabled state (SC 4.1.2)
- Support for newer APIs: Popover API, Invoker Commands API, Interest Invokers API
Step 1: The Empty Nucleus
Start with a custom element. It behaves like a `` by default, so you add role="button":
A button from scratch
Already you're behind native `` which has the role built-in.
Step 2: Accessible Label
Icon buttons break accessibility. An emoji alone fails WCAG SC 2.4.6 and SC 4.1.2. You need aria-label and hide the emoji:
💩
Step 3: Focusability
Add tabindex="0" to make the element keyboard-focusable. Positive values are discouraged as they mess up tab order.
Step 4: Pointer Events
You need mouseup, touchend, and pointerup events. Browsers map some of these to click, but for completeness, implement all three:
Step 5: Keyboard Events
Space and Enter must trigger the action. This requires both keydown (for Enter) and keyup (for Space) listeners, with careful checks to avoid double-firing:
handleEvent(evt) {
switch (evt.type) {
case 'keyup':
this.#handleSpaceActivation(evt);
break;
case 'keydown':
this.#handleEnterActivation(evt);
break;
}
}
#handleSpaceActivation(evt) {
if (evt.key !== ' ') return;
if (document.activeElement !== this) return;
this.#doButtonAction(evt);
}
#handleEnterActivation(evt) {
if (evt.key !== 'Enter') return;
this.#doButtonAction(evt);
}
Step 6: The Disabled State
Native `` handles disabled out of the box. For a custom element, you must:
- Add
disabledtoobservedAttributes - Sync
ariaDisabledproperty - Add a custom state for CSS styling
static observedAttributes = ['disabled'];
set disabled(newValue) {
this.toggleAttribute('disabled', newValue);
this.#internals.ariaDisabled = newValue ? 'true' : 'false';
if (newValue) {
this.#internals.states.add('--disabled');
} else {
this.#internals.states.delete('--disabled');
}
}
get disabled() {
return this.hasAttribute('disabled');
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
switch (name) {
case 'disabled':
this.disabled = newValue !== null;
break;
}
}
CSS: sagan-button:state(--disabled) { ... }
Step 7: Form Participation
A button can submit or reset a form. That means implementing type, form, formaction, formenctype, formmethod, formnovalidate, formtarget, name, and value attributes. Each requires its own getter/setter and attribute change handler. The article notes this is "a lot of attributes" – understatement.
Step 8: Modern APIs
The native `` already supports the Popover API (popovertarget), Invoker Commands, and Interest Invokers. Your custom element must implement each from scratch.
The Verdict
Madcampos doesn't finish the implementation – the article stops mid-sentence at the form attributes section. That's the point: it's so much work that even the author gave up listing everything. The article is a cautionary tale: don't rebuild what the platform gives you for free.
Why This Matters
Every time you reach for a `` with an ARIA role, you're signing up for months of maintenance. Native elements are tested across browsers, assistive technologies, and edge cases. Your custom button will never be as robust.
Next Steps
Audit your codebase for custom interactive elements. Replace with. For complex widgets, use established patterns from the ARIA Authoring Practices Guide. Your users – and your future self – will thank you.


