AuroraUI. Components. Progress
(Aurora UI v0.1.0)
View Source
Progress family — spinner, progress, skeleton, and an async_state
wrapper for LiveView async assigns and streams.
Together these cover the "we are working / waiting on data" surface:
spinner/1— an indeterminate busy indicator for short, unmeasurable waits (a submit in flight). It always carries a visually-hiddenlabeland lives in arole="status"region so assistive tech announces it. Underprefers-reduced-motionit slows down rather than stopping, so it never reads as frozen.progress/1— a labelled progress bar. Determinate mode is a realrole="progressbar"witharia-valuenow/min/max(and an optional visible percentage); the indeterminate variant dropsaria-valuenowand shows a sweeping fill.skeleton/1— a layout-reserving placeholder. You pass explicitwidth/height/shapeso the box occupies the same space the real content will, avoiding cumulative layout shift. It isaria-hiddenand its shimmer is disabled under reduced motion.async_state/1— a single wrapper that renders one of itsloading/empty/error/inner_blockslots based on astateatom. It is designed to sit directly over a LiveViewassign_async/stream result.
async_state/1 with LiveView async + streams
Map the AsyncResult (and stream emptiness) to a single state atom, then let
the wrapper pick the branch:
# mount / handle_params
socket
|> assign(:page, AsyncResult.loading())
|> start_async(:load, fn -> Catalog.list() end)
def handle_async(:load, {:ok, rows}, socket) do
{:noreply,
socket
|> assign(:page, AsyncResult.ok(socket.assigns.page, :loaded))
|> stream(:rows, rows)}
end
# template — one branch renders at a time; the :ok branch streams
<.async_state state={async_state(@page, @streams.rows)}>
<:loading><.skeleton :for={_ <- 1..5} height="2.5rem" /></:loading>
<:empty><.empty_state title="No results" /></:empty>
<:error>Could not load. <.button phx-click="retry">Retry</.button></:error>
<div id="rows" phx-update="stream">
<div :for={{id, row} <- @streams.rows} id={id}>{row.name}</div>
</div>
</.async_state>
# a tiny mapper in your LiveView keeps the template declarative
defp async_state(%AsyncResult{loading: l}, _) when not is_nil(l), do: :loading
defp async_state(%AsyncResult{failed: f}, _) when not is_nil(f), do: :error
defp async_state(%AsyncResult{ok?: true}, []), do: :empty
defp async_state(%AsyncResult{ok?: true}, _), do: :ok
Summary
Functions
Renders exactly one of its slots based on state — the declarative counterpart
to a case over a LiveView async/stream result. See the moduledoc for the
assign_async + streams wiring.
A progress bar. Determinate by default (pass value); becomes indeterminate
when value is nil or indeterminate is set.
A layout-reserving placeholder. Always give it the size the real content will
occupy so it prevents cumulative layout shift. It is decorative
(aria-hidden); announce the overall busy state at a higher level (a
role="status" region, spinner, or async_state).
An accessible indeterminate busy indicator.
Functions
Renders exactly one of its slots based on state — the declarative counterpart
to a case over a LiveView async/stream result. See the moduledoc for the
assign_async + streams wiring.
Examples
<.async_state state={@state}>
<:loading><.spinner label="Loading orders" /></:loading>
<:empty>No orders yet.</:empty>
<:error>Something went wrong.</:error>
<.orders_table rows={@orders} />
</.async_state>Attributes
state(:atom) - which branch to render. Defaults to:ok. Must be one of:loading,:empty,:error, or:ok.- Global attributes are accepted.
Slots
loading- shown whilestateis :loading (defaults to a spinner).empty- shown whilestateis :empty.error- shown whilestateis :error (rendered in a role=alert region).inner_block(required) - the loaded content, shown whilestateis :ok.
A progress bar. Determinate by default (pass value); becomes indeterminate
when value is nil or indeterminate is set.
Determinate bars are a real role="progressbar" exposing
aria-valuenow/aria-valuemin/aria-valuemax; the indeterminate variant
omits aria-valuenow so AT announces "busy" rather than a false number.
Examples
<.progress value={64} label="Uploading" show_value />
<.progress indeterminate label="Processing" />Attributes
value(:integer) - current progress; when nil the bar is indeterminate. Defaults tonil.max(:integer) - Defaults to100.min(:integer) - Defaults to0.label(:string) - concise visible label describing the task. Defaults tonil.show_value(:boolean) - renders the percentage next to the label. Defaults tofalse.indeterminate(:boolean) - force the indeterminate variant even if a value is present. Defaults tofalse.size(:string) - Defaults to"md". Must be one of"sm","md", or"lg".- Global attributes are accepted.
A layout-reserving placeholder. Always give it the size the real content will
occupy so it prevents cumulative layout shift. It is decorative
(aria-hidden); announce the overall busy state at a higher level (a
role="status" region, spinner, or async_state).
Examples
<.skeleton width="12rem" height="1.25rem" shape="text" />
<.skeleton width="3rem" height="3rem" shape="circle" />Attributes
width(:string) - explicit width (any CSS length) to reserve layout. Defaults to"100%".height(:string) - explicit height to reserve layout. Defaults to"1rem".shape(:string) -textuses a line height + rounded ends;circleforces a 1:1 avatar. Defaults to"rect". Must be one of"rect","text","circle", or"pill".- Global attributes are accepted.
An accessible indeterminate busy indicator.
When not to use
If the wait is measurable, prefer progress/1 with a value so the user sees
how far along they are.
Examples
<.spinner label="Saving changes" />
<.spinner size="lg" label="Loading dashboard" />Attributes
label(:string) - visually-hidden accessible name announced while busy. Defaults to"Loading…".size(:string) - Defaults to"md". Must be one of"sm","md", or"lg".- Global attributes are accepted.