forty-cdk
llms.txt

Primitives

Button

Turns any element (a native <button> or a custom host like <div> / <span>) into an accessible button with keyboard activation. Disabled stays focusable (aria-disabled, never the native attribute) and pressed / hovered / focus-visible are reflected as data-* hooks.

forty-cdk/button WAI-ARIA APG

Press and hold either control (a native <button> and a <span>) and watch data-pressed, data-hovered and data-focus-visible appear on both, so one rule styles the pair.

Custom <span>

A single [forButton] directive does all of this. On a native <button> host the platform owns Enter/Space activation and type handling; on any non-button host the directive adds role="button", tabindex="0", and keyboard activation so the contract matches.

Anatomy

<!-- Native button — platform owns Enter/Space and type handling -->
<button forButton [disabled]="saving()" (activate)="save()">Save</button>

<!-- Non-button host — role="button", tabindex="0", keyboard activation added -->
<div forButton (activate)="save()">Save</div>

Examples

Disabled stays focusable

Per the APG, a disabled button must stay reachable so assistive tech can announce it. forButton never sets the native disabled attribute. Instead, it reflects aria-disabled='true' + data-disabled and makes activation a no-op. The native disabled button is skipped entirely.

forButton [disabled]focusable · aria-disabled
native <button disabled>removed from tab order

Disabled

Disabled buttons stay focusable so assistive technology can announce them. The native disabled attribute is never set; instead aria-disabled="true" is reflected.

<button forButton [disabled]="isSaving()" (activate)="save()">Save</button>

A surrounding disabled [forFieldset] disables the button too: its disabled input is OR'd with the group's, so aria-disabled / data-disabled are reflected and activation is suppressed. This matters most on a non-native host (<div forButton>), which a native <fieldset disabled> cannot reach.

<fieldset forFieldset [disabled]="locked()">
  <legend forFieldsetLegend>Account</legend>
  <button forButton (activate)="save()">Save</button>
</fieldset>

Preserve consumer type

A native <button> without an explicit type attribute defaults to type="button". A consumer-set type="submit" is preserved:

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

API

ForButton

PropertyTypeDescription
disabled
input
Suppresses activation and reflects aria-disabled + data-disabled. OR'd with a surrounding [forFieldset]'s disabled state.
Default: false
activate
output
Fires once per user activation (click, Enter, Space). Never fires when disabled.
Default: —

The directive reflects boolean data-* attributes (present with an empty-string value when true, absent when false). There is no data-state, because this primitive has no open/closed or checked/unchecked logical state.

Data attributeValues
data-disabledpresent | absent
data-pressedpresent | absent
data-hoveredpresent | absent
data-focus-visiblepresent | absent

data-pressed is present while the primary pointer is held down or Enter/Space is held. data-hovered is present while a mouse/pen pointer is over the element. data-focus-visible is present when focused via keyboard (keyboard modality active).

Keyboard

On a native <button> host the platform owns activation: every key handler the directive binds returns immediately, and nothing in this table is its doing. On any other host (<div forButton>, <span forButton>) it synthesizes the activation itself, through the same (click) path a pointer takes. The split between the two keys is deliberately asymmetric, because that is what a native button does.

KeyAction
Enter on a native <button>The platform synthesizes the click; the directive adds nothing.
Enter on any other hostActivates on keydown, so (activate) fires while the key is still held. While disabled the event is left alone entirely: not even its default is prevented.
Space on a native <button>The platform synthesizes the click on release and suppresses the page scroll itself.
Space on any other hostActivates on keyup, and only when the matching keydown reached the same host. Focus leaving mid-press drops the press. Its keydown always calls preventDefault() to stop the page scrolling, even while the button is disabled.

Both keys drive data-pressed on every host: present from keydown until keyup, until focus leaves, or until the pointer is released. data-focus-visible instead follows the keyboard modality, so a keydown carrying Meta / Control / Alt is read as a shortcut and does not turn it on, while Shift does.

Accessibility

Implements the WAI-ARIA Button pattern.

  • Native <button> semantics are preserved. On a native host, no extra ARIA is added; the browser's built-in button role, keyboard activation, and type handling all apply.
  • Non-button hosts get role="button" and tabindex="0", and the directive synthesizes the activation the platform would have (see Keyboard).
  • Disabled buttons stay focusable. aria-disabled="true" is used instead of the native disabled attribute so assistive technology can still announce the control's purpose.

Styling

forty-cdk ships no styles: put your own class on each piece and key your CSS off the data-* attributes listed under API, not off the for* selectors (Styling forty-cdk explains why).

[forButton][data-disabled] {
  opacity: 0.4;
  pointer-events: none;
}

[forButton][data-pressed] {
  transform: scale(0.97);
}