A button asks the application to do something: recalculate a preview, save a form, or open a dialog. Start with component construction and native event handlers. The important design decision is the action and its focus destination; the visual treatment comes afterward.
Button renders a real <button>. Its ButtonOptions requires content, accepts reactive type and disabled, and accepts an onclick handler through the shared DOM boundary. The default type is "button", so placing an action inside a form does not unexpectedly submit it. There is no internal click action or application state to configure.
Make the action visible in state
This preview counter is a complete renderable component. Activation updates a subject; the same subject supplies the displayed count. Nothing starts until the component runs under the renderer described in Mounting DOM output.
import { RefSubject } from "@typed/fx";
import { html } from "@typed/template";
import { Button } from "@typed/ui/Button";
import { component } from "@typed/ui/Component";
export const PreviewCounter = component(function* () {
const previews = yield* RefSubject.make(0);
return html`<section>
${Button({
content: "Recalculate preview",
props: { class: "preview-action" },
onclick: RefSubject.update(previews, (count) => count + 1),
})}
<p role="status">Preview calculations: ${previews}</p>
</section>`;
});
The handler is an Effect, not a manually launched promise. Its required services and failures remain part of the renderable type. An actual calculation belongs in that handler or a service it calls; the counter here deliberately demonstrates activation rather than pretending to perform work.
Distinguish activation from submission
The APG button pattern describes Space and Enter activation and context-dependent focus after an action. The native host supplies keyboard activation; do not add a second keydown handler that also invokes the action. That commonly produces two operations for one key press.
Use type: "submit" for submission and put validation and the request on the form’s submit boundary. Use type: "reset" only when resetting the surrounding form is intentional; Form coordinates native reset with Typed state. disabled uses the native disabled property. aria-disabled alone describes unavailability but does not implement the same interaction suppression.
A toggle command needs an explicit state model and aria-pressed; Button does not infer that state from repeated clicks. An on/off preference may be better represented by Switch. A destination belongs to Link, even if the design gives it a filled button appearance.
Style the host without replacing its contract
Use props.class and design tokens for foreground, background, border, and focus ring. Keep a visible :focus-visible treatment and distinguish disabled state through more than opacity alone. Content is the default accessible name; an icon-only button needs a meaningful aria-label or VisuallyHidden content.
The second argument can render a custom host with html, spreading the supplied props and rendering the supplied content. Keeping a <button> preserves native behavior. Replacing it with a <div> makes keyboard handling, focusability, disabled behavior, and form participation your responsibility; spreading props alone cannot restore those browser contracts.
When an action fails to fire, inspect the rendered type/disabled state and the handler’s Effect error channel before adding event listeners. When it fires twice, check for both keydown and click execution or a submit handler combined with a submitting button’s request handler. See the Button API for Button, ButtonOptions, and ButtonType.