Interact
Manage disabled state, tabindex, and activation blocking on any interactive element.
Why use this atom?
- Unified disabled handling for native and non-native elements
- Hard disable: native
disabledattr,tabindex=-1, removed from tab order - Soft disable:
aria-disabled, keeps tabindex, blocks Enter/Space/click but allows arrow keys data-disabled="hard"ordata-disabled="soft"for CSS targeting- Click blocking via capture-phase listener — fires before template
(click)handlers - Keyboard suppression only blocks activation keys (Enter/Space), not navigation keys
Import
ts
import {AtomInteract} from '@terse-ui/atoms/interact';Usage
Hard disable (default)
html
<button atomInteract [disabled]="loading()">Save</button>On native elements, sets the native disabled attribute. On non-native elements (<div>, <span>, <a>), sets aria-disabled="true" and tabindex="-1".
Soft disable (keeps focus)
html
<button atomInteract [disabled]="loading()" [disabledInteractive]="true">Save</button>The element stays focusable and in the tab order. Uses aria-disabled instead of native disabled. Click and Enter/Space are blocked, but arrow keys, Escape, Home, End, and Tab pass through — so parent containers (menus, listboxes, toolbars) can still navigate through soft-disabled items.
This follows the APG guidance on focusability of disabled controls.
As a host directive
ts
@Directive({
selector: '[protoButton]',
hostDirectives: [
{directive: AtomInteract, inputs: ['disabled', 'disabledInteractive', 'tabIndex']},
],
})
export class ProtoButton {}Styling
css
[atomInteract][data-disabled="hard"] {
opacity: 0.5;
pointer-events: none;
}
[atomInteract][data-disabled="soft"] {
opacity: 0.7;
cursor: not-allowed;
}Loading state pattern
html
<button
atomInteract
[disabled]="isLoading()"
[disabledInteractive]="isLoading()"
>
{{ isLoading() ? 'Loading...' : 'Submit' }}
</button>When loading starts, the button becomes soft-disabled — keyboard users keep their focus position. When loading finishes, the button re-enables without moving focus.
API Reference
AtomInteract
| Selector | [atomInteract] |
| Exported as | atomInteract |
| Data attributes | data-disabled |
| ARIA bindings | aria-disabled |
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
disabled | boolean | ||
disabledInteractive | boolean | ||
tabIndex | number |
Properties
| Property | Type | Description |
|---|---|---|
disabledControl | WritableSignal<boolean> & { control(binder: (current: boolean) => boolean | undefined, opts?: { injector?: Injector; options?: CreateEffectOptions; track?: boolean; } | undefined): EffectRef; } (readonly) | |
disabledInteractiveControl | WritableSignal<boolean> & { control(binder: (current: boolean) => boolean | undefined, opts?: { injector?: Injector; options?: CreateEffectOptions; track?: boolean; } | undefined): EffectRef; } (readonly) | |
tabIndexControl | WritableSignal<number> & { control(binder: (current: number) => number | undefined, opts?: { injector?: Injector; options?: CreateEffectOptions; track?: boolean; } | undefined): EffectRef; } (readonly) | |
hardDisabled | Signal<boolean> (readonly) | |
softDisabled | Signal<boolean> (readonly) |