Browse documentation

UI

Choosing Typed UI components

Develop a report screen by assigning each interaction to the browser, a Typed family, or application state.

Imagine a report screen with a date range, an explanation of its calculations, a refresh action, and an archive confirmation. Every piece could be drawn as a rectangle. That tells you very little about which component to use. The useful questions are what the person is doing, what must remain available to keyboard users, and who owns the resulting state.

This lesson starts with ordinary markup and introduces a Typed abstraction only when a concrete requirement needs it. Read your first template first; building UI components continues into reusable application APIs.

Let document content stay document content

A report title, calculation explanation, and link to another page need no component constructor or state allocation. The browser already knows how to expose a heading, follow a link, and toggle native details. Write the document directly:

import { html } from "@typed/template";

const reportIntroduction = html`
  <header>
    <h1>Quarterly revenue</h1>
    <p>Recognized revenue for the selected period.</p>
    <a href="/reports/methodology">Read the reporting methodology</a>
  </header>
  <details>
    <summary>How these totals are calculated</summary>
    <p>Refunds reduce revenue in the period in which they are issued.</p>
  </details>
`;

There is no reason to allocate a RefSubject simply because the details element has an open state. That state can remain entirely native until another part of the application needs to read or change it. Nor does repeating a few HTML elements automatically justify a generator: a plain function returning html is enough for parameterized markup without Effectful setup.

The native details element keeps the explanation in document flow. That is a product decision: opening it moves later content rather than covering the report. If the explanation needs to stay available while the user reads, this behavior may be preferable to any floating surface.

Add state where two parts must agree

Now suppose the report footer should say whether the calculation explanation is expanded, and an error message elsewhere should be able to reveal it. A native element alone no longer expresses the whole relationship. Disclosure connects browser state and Typed state while retaining details and summary semantics.

import { RefSubject } from "@typed/fx";
import { html } from "@typed/template";
import { component } from "@typed/ui/Component";
import * as Disclosure from "@typed/ui/Disclosure";

const ReportExplanation = component(function* () {
  const state = yield* Disclosure.makeState();
  const availability = RefSubject.map(state, ({ open }) =>
    open ? "Calculation details are expanded above." : "Expand calculation details for an explanation.");
  return html`
    ${Disclosure.Content({
      state,
      content: [
        Disclosure.Button({ content: "How these totals are calculated" }),
        html`<p>Refunds reduce revenue in their issue period.</p>`,
      ],
    })}
    <p>${availability}</p>
  `;
});

The generator now earns its place: it acquires state for this rendered instance. The family sends native toggle events into that state and state updates back into the element. The footer derives its text from the same source. A second application boolean would create a synchronization problem instead of solving one. Disclosure develops that contract fully.

Separate actions, values, and destinations

Refreshing the report is an action. Selecting its date range changes a value. Opening a saved report navigates to a destination. They may share styling, but their browser behavior should differ.

Person’s intentStart withConsequence for the application
Refresh the current reportNative button or ButtonSupply an Effect; decide busy and failure behavior.
Move to another report URLAnchor or LinkPreserve link navigation and browser affordances.
Choose one valueNative select, Select, or RadioGroupOwn selected data and its form representation.
Search a large set by typingComboboxCoordinate query text, active match, and committed selection.
Invoke one of several commandsMenuProvide command effects and menu keyboard behavior.

A command menu is therefore a poor date-range field: its selected-looking row does not establish form submission or persistent value ownership. Conversely, a Select whose options perform unrelated actions disguises commands as data. For submitted values, start with Form so validation and transport belong to the same design as the visible controls.

Keep the screen policy outside the primitive

A refresh button needs a label, native activation, and disabled behavior. Whether refreshing replaces a cache, reports a recoverable error, or continues after leaving the page belongs to the application. This direct composition needs no generator because the caller has already supplied the work:

import { Effect } from "effect";
import { html } from "@typed/template";
import * as Button from "@typed/ui/Button";

const reportActions = <E, R>(refresh: Effect.Effect<void, E, R>) => html`
  <nav aria-label="Report navigation"><a href="/reports">All reports</a></nav>
  ${Button.Button({ content: "Refresh report", onclick: refresh })}
`;
const actions = reportActions(Effect.log("Refresh requested"));

When the action needs local busy/error state, introduce a component and acquire that state there. Building UI components shows the complete progression. Let the result retain the action’s error and service requirements; do not hide missing services with casts or turn every failure into a generic successful message.

Choose a larger interaction only when the task needs it

Archiving asks the user to make a decision in a separate task; a modal Dialog can give that task focus and make the report inert while it is open. A chart legend merely supplements the report; a Popover can show it without claiming modal behavior. A short button description belongs in a Tooltip; a preview with a profile link needs Hovercard or ordinary visible content.

Use the overlay lesson to work through these consequences. The APG patterns are a reference for interaction expectations, not a menu of roles to sprinkle over unrelated markup.

Recognize when you are authoring a new family

Changing colors or rearranging public parts does not require a new behavior primitive. Use props for styling and a family’s host argument when its real semantic element needs custom presentation. Forward the complete props object so refs, events, and state attributes remain attached.

Reach for Dom, Collection, and Composite when no existing family implements a reusable interaction you actually need. At that point you own the state machine, focus behavior, relationships, disabled and removal policies, and browser evidence. A role and an arrow-key handler alone are not a finished family.

Validate the decision at the user boundary: Can a keyboard user reach and leave the report actions? Does a field submit the chosen value? Does cancellation avoid archiving? Does the explanation remain readable after zooming? These checks reveal a wrong abstraction sooner than inspecting whether every screen fragment has a component name.

Interaction contracts by family

Components provide specific behavior. Labels, content, and composition still belong to the application.

Actions and navigation

Button · Link

Typed supplies

  • Button renders a native button with a non-submitting default type and native disabled behavior.
  • Link renders a native anchor and only intercepts eligible same-origin primary clicks.

The application supplies

  • An accessible name through visible content or another valid naming mechanism.
  • A destination and link-versus-action choice that matches the user’s intent.
Standards and implementation evidence

Boolean and exclusive choice

Checkbox · Switch · RadioGroup

Typed supplies

  • Checkbox derives native checked, indeterminate, and aria-checked state from one subject.
  • Switch and RadioGroup expose their checked state and keyboard collection behavior through their public parts.

The application supplies

  • A visible or programmatic label, meaningful option text, and the correct single-versus-multiple-choice model.
  • A custom host that keeps the supplied checked, disabled, event, and ref props.
Standards and implementation evidence

Numeric input and measurement

Slider · SpinButton · Meter

Typed supplies

  • Slider and SpinButton synchronize finite state with native range and number inputs.
  • Meter renders the native meter element from finite state and range props.

The application supplies

  • A label, units, min/max/step values, and a value meaning that a person can understand.
  • A non-native host only when it preserves the supplied range and value semantics.
Standards and implementation evidence

Forms and validation

Form

Typed supplies

  • Form supplies native form controls, field metadata, aria-describedby/aria-invalid projection, and alert-role error output.
  • Submit and reset behavior remains attached to native form semantics.

The application supplies

  • Field labels, instructions, error wording, required constraints, and a validation policy appropriate to the domain.
  • Server-side validation and an error-recovery flow; a client component cannot establish either alone.
Standards and implementation evidence

Disclosure and overlays

Disclosure · Dialog · Popover · Tooltip · Hovercard · NativeDetails · NativeDialog · NativePopover

Typed supplies

  • Disclosure, Dialog, and Popover synchronize one open state with the corresponding native details, dialog, or popover lifecycle.
  • Tooltip and Hovercard supply their documented trigger/content roles and relationships, including Escape handling where implemented.

The application supplies

  • A useful trigger name, a dialog label/description when required, and content that is appropriate for a transient overlay.
  • Focus-return, dismissal, modality, and reading-order choices when composing outside the public family contract.
Standards and implementation evidence

Popup selection and autocomplete

Menu · Select · Combobox · Listbox

Typed supplies

  • The public parts provide menu, listbox, option, and combobox roles plus their state-derived relationships.
  • Collections drive active identity, enabled-item ordering, and the keyboard/typeahead behavior implemented by each family.

The application supplies

  • Stable item ids, accurate option labels, and content whose meaning matches the chosen interaction pattern.
  • A selection and filtering policy; Typed cannot infer whether every source value should be selectable or visible.
Standards and implementation evidence

Composite navigation

Tab · Tabs · Toolbar · Menubar

Typed supplies

  • Tabs supplies tablist, tab, and tabpanel relationships from shared state and collection identity.
  • Toolbar and Menubar supply their roles, orientation, active-item state, and collection-driven keyboard behavior.

The application supplies

  • A concise accessible name where the pattern needs one and items with meaningful labels in a logical order.
  • A deliberate activation and navigation policy when mixing controls with different behavior.
Standards and implementation evidence

Structured navigation

Tree · Grid · TreeGrid

Typed supplies

  • Tree, Grid, and TreeGrid derive public roles, active identity, and item/row/cell relationships from their state and collections.
  • Grid and TreeGrid keep their composite focus model at the root through aria-activedescendant when that pattern is used.

The application supplies

  • A stable, meaningful hierarchy or table model; correct row, cell, and header content; and labels that explain the collection.
  • Data-loading, editing, sorting, and selection semantics beyond the public component state contract.
Standards and implementation evidence

Layout and rotation controls

Carousel · WindowSplitter

Typed supplies

  • Carousel supplies region/group structure, active-slide state, and its documented previous/next/rotation controls.
  • WindowSplitter supplies a focusable separator role with orientation and range values from shared state.

The application supplies

  • An accessible carousel name, meaningful slide content, and a rotation policy that is appropriate for the page.
  • Pane labels, a usable collapsed-state policy, and content that remains understandable at all splitter sizes.
Standards and implementation evidence

Semantic primitives

Alert · Group · Heading · Separator · Focusable · Role · VisuallyHidden

Typed supplies

  • These families render their documented native host or explicit ARIA role, level, orientation, focusability, or visually-hidden treatment.
  • Alert, heading, group, and separator semantics are supplied as props rather than inferred from styling.

The application supplies

  • Accurate role selection, headings in a meaningful document outline, and labels for groups or focusable content when needed.
  • Announcement timing and message priority; role=alert must not be used for routine static content.
Standards and implementation evidence