PUI Domain Model

Copy Markdown

PUI is a reusable UI composition context for Phoenix LiveView. Its boundary is the interface between a host application's server-rendered view state and the browser's interactive presentation layer.

PUI does not own business entities, persistence, authorization, routing policy, or application workflows. A host application owns those concerns and supplies values, events, form state, and business meaning to PUI.

Context boundary

ParticipantOwnsHands across the boundary
Host applicationBusiness meaning, LiveView state, events, forms, and persistenceAssigns, slots, IDs, values, JS commands, and event handlers
PUI server componentsHTML structure, semantic relationships, default presentation, normalization, and server-facing value plumbingRendered markup, ARIA state, data-* markers, hidden fields, and hook declarations
PUI browser hooksEphemeral interaction, focus, keyboard navigation, positioning, resizing, animation state, and client-only persistenceNative events, custom PUI events, LiveView events, and DOM state
Host application's design systemBrand presentation, icon vocabulary, and application-specific compositionCSS variables, utility classes, icon providers, custom slots, and primitive-module classes

The boundary is intentionally not a client/server ownership split for every value. A selected form value may be server-owned while the open state of the select popup is browser-owned. The important question is whether the state must participate in LiveView rendering and application behavior.

Component taxonomy

AreaComponent familiesPrimary responsibility
Composition and structureAccordion, Avatar, Breadcrumb, ButtonGroup, Card, Container, Empty, Layout, Separator, SkeletonGroup content and actions into reusable structural shapes
Actions and navigationButton, DropdownProvide actionable controls, links, menu triggers, and menu items
FormsInput, Select, DatePickerRender field-aware controls, normalize values, and expose validation feedback
Disclosure and overlaysDialog, Popover, Tooltip, TabsCoordinate triggers, content, focus, visibility, and relationships
FeedbackAlert, Badge, Flash, Loading, ProgressCommunicate status, transient messages, progress, and loading
Data displayBadge, Empty, Pagination, Progress, TablePresent compact labels, completion values, and collections
Data visualizationChart, BarChart, LineChartRender serializable chart configuration and data through browser chart hooks
Shared vocabularyComponents, icon providerSupply small cross-family presentation and error helpers while leaving icon-library choice to the host

Accordion is the native-behavior exception within the interactive families: it uses <details> and <summary> so its core expand/collapse behavior does not require a JavaScript hook. Other richer interactions are progressively enhanced with hooks or LiveView JS commands.

Tables are presentation primitives rather than data-querying components. A table can render ordinary lists or LiveView streams and expose row/slot callbacks, but the host application owns sorting, filtering, pagination, authorization, and data loading.

Three usage levels

PUI exposes one composition model at three levels:

  1. Styled — use a component's default visual treatment and customize it with supported attributes and classes.
  2. Unstyled/headless — retain PUI's structure and interaction semantics while supplying the visual treatment, including visibility classes where the host owns them.
  3. Low-level hook — compose the surrounding markup directly and satisfy the hook contract yourself.

The levels are not separate product lines. They are points on the same spectrum of responsibility: PUI supplies more presentation at the styled level and transfers more markup and styling responsibility to the host at the low-level end.

Composition rules

PUI component families generally follow these rules:

  • The outer primitive establishes the relationship and state boundary.
  • Slots provide consumer-owned content without requiring a host wrapper to duplicate the family structure.
  • IDs connect triggers, content, labels, panels, and hidden form fields.
  • Roles and ARIA attributes remain part of the contract in the .Primitive modules.
  • A class passed to a styled component augments its default presentation; a primitive part treats consumer classes as its entire visual implementation.
  • Global attributes are forwarded only where the component's public contract allows them.
  • PUI-owned icon intent crosses the host boundary as an icon token; application-owned icons cross it as slot content.

For hook-backed families, the markup is an API. A change to a data-pui marker, derived ID, hook name, role, or event name can break browser behavior even when the rendered HTML still looks correct.

State ownership and lifecycle

Server-owned values

The server owns values that must be submitted, validated, rendered into a new LiveView state, or observed by application code. Field-backed inputs derive identity and values from Phoenix.HTML.FormField. Selects and date pickers expose their current selection through form-compatible inputs and native change events. Dialogs can also be controlled by a server show value and an on_cancel command that keeps server state aligned when the user dismisses the dialog in the browser.

Browser-owned interaction

Hooks own short-lived interaction details: popup placement, outside-click handling, keyboard roving focus, active descendants, tooltip positioning, chart measurement, loading-bar progress, and sidebar collapse persistence. These values are restored or recalculated during the hook lifecycle rather than becoming application state by default.

LiveView patch lifecycle

Hook-backed components must tolerate the full lifecycle:

  1. mounted discovers the component's semantic elements and binds listeners.
  2. updated rereads server-rendered attributes, restores relevant browser state, and rebinds if the DOM changed.
  3. destroyed removes listeners, observers, timers, and floating-positioning resources.

This lifecycle is why stable IDs and marker attributes matter. The server may replace or patch the markup while the browser interaction is open.

Core interaction workflows

Overlay workflow

The component renders a trigger/content relationship and a hook or JS command controls open state, focus, and positioning. The hook updates ARIA state and restores focus on close; the host application may additionally send a LiveView event when the business workflow needs to know that the overlay was dismissed.

This covers popovers, dropdowns, tooltips, selects, and date pickers, with dialog adding focus wrapping and server-controlled visibility.

Form workflow

The host supplies either a field-backed control or explicit values. PUI normalizes identity and values, renders the semantic control and validation feedback, and lets the browser emit native input/change events. The host form or LiveView decides what validation or business action follows.

Select and date-picker implementations may use hidden native inputs because the visible trigger is not itself the submitted form control. Those hidden inputs are part of the public behavior contract, not an implementation detail consumers should replace.

Feedback workflow

The host application creates an alert, flash message, loading state, progress value, or badge value. PUI renders it and, where appropriate, manages display-only behavior such as auto-dismissal, streaming updates, close controls, or a transition loading bar. PUI does not infer why the status exists.

Layout workflow

The host composes a layout shell from structural primitives. Sidebar collapse and submenu expansion are browser interaction state with a small synchronization seam: the hook can persist collapse state in session storage and emit a pui:sidebar-collapsed event when the host needs to react.

Extension and change rules

When extending PUI:

  • Preserve semantic elements, roles, relationships, and keyboard behavior before changing presentation.
  • Treat hook names, stable IDs, data-* markers, native input events, and custom PUI events as versioned contracts.
  • Keep business decisions in the host application; accept values and events rather than introducing application policy into a primitive.
  • Prefer native HTML behavior when it satisfies the interaction, as with Accordion.
  • Keep browser-only state local to hooks unless the host explicitly needs it in LiveView state.
  • If a family has both a styled component and a .Primitive module, make the semantic contract identical unless the API explicitly documents a deliberate difference.
  • Add deterministic rendered-markup tests for server contracts and browser-level coverage for hook behavior when the behavior cannot be proven from markup alone.

Current boundary notes

  • use PUI is the primary convenience surface for the main component families. PUI.Flash and PUI.Loading remain directly addressed modules rather than imports from the macro.
  • Canonical component modules own one component family and are the preferred public namespace for new code. Shared helpers remain grouped when they support multiple families without owning a visual primitive.
  • The current package contains both PUI.Dropdown.menu_button/1 and a separate PUI.MenuButton module. The use PUI macro imports PUI.Dropdown; the latter is therefore the canonical documented menu-button path until the duplicate surface is intentionally consolidated.
  • The package's public hook registry includes loading, popover, date picker, select, tabs, tooltip, flash group, sidebar, and chart hooks. A component that renders a hook declaration still requires the consuming application to merge PUI's exported hooks into its LiveSocket.
  • The demo application is a consumer and executable documentation surface. It demonstrates the package, but it is not part of PUI's host-application domain boundary.