Loading Overlay

v0.0.10

v0.0.10StableHeadlessStatelessAccessibleApache-2.0
Installation mix mishka.ui.gen.headless loading_overlay
Source View Source

Overview of the Phoenix and LiveView Headless Loading Overlay Component

The Mishka Chelekom Headless Loading Overlay covers a container with a loader while something loads. It is an unstyled Phoenix function component that renders one element with role="status", aria-live="polite" and an aria-label. You put a spinner or text in the default slot. visible sets data-visible on the element, and your Tailwind classes decide how it fades. No JavaScript is involved.

While visible is false, the element gets aria-hidden="true" and an inline pointer-events: none, so a faded-out overlay never blocks clicks on the content under it. In a LiveView you flip visible from an assign. Your classes only own the looks, for example opacity-0 whenever data-visible is missing.

Base UI Loading Overlay example - loader over a card (Phoenix headless)

Base UI Loading Overlay example - loader over a card (Phoenix headless)

Content behind the overlay…

heex
<div class="relative h-32 w-64 overflow-hidden rounded-lg border border-neutral-300 p-4 text-sm text-neutral-700 dark:border-neutral-700 dark:text-neutral-300">
  <p>Content behind the overlay…</p>
  <.loading_overlay
    visible
    label="Loading"
    class="absolute inset-0 flex items-center justify-center bg-white/70 backdrop-blur-sm dark:bg-neutral-900/70 [&:not([data-visible])]:pointer-events-none [&:not([data-visible])]:opacity-0"
  >
    <span class="size-6 animate-spin rounded-full border-2 border-neutral-300 border-t-neutral-900 dark:border-neutral-600 dark:border-t-white"></span>
  </.loading_overlay>
</div>

Showing and hiding from the server

Bind visible to an assign and change the assign in your LiveView. The overlay stays in the DOM, so the fade works in both directions.

heex
<div class="relative h-40 w-72 overflow-hidden rounded-lg border p-4">
  <h4>Quarterly report</h4>
  <p>The numbers keep loading behind this overlay.</p>
  <.loading_overlay
    visible={@loading}
    label="Loading report"
    class="absolute inset-0 flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 [&:not([data-visible])]:pointer-events-none [&:not([data-visible])]:opacity-0"
  >
    <span class="size-6 animate-spin rounded-full border-2 border-neutral-300 border-t-neutral-900"></span>
  </.loading_overlay>
</div>
<button type="button" phx-click="toggle">Toggle loading</button>
elixir
def handle_event("toggle", _params, socket) do
  {:noreply, update(socket, :loading, &(!&1))}
end

Attributes

visible is a boolean and defaults to false. label is written to aria-label and defaults to Loading. id is optional. class goes on the root, and any other attribute, such as data-* or aria-*, is passed to it too. The default slot holds the loader.

Parts & data attributes

The component has one element, marked data-part="root". It carries data-visible when the overlay is shown and aria-hidden="true" when it is not. The structural class is chelekom-loading-overlay, which you can target from your own CSS.

Accessibility

The overlay is a status live region named by label. Give it a label that says what is loading, such as Loading report. While it is hidden it is removed from the accessibility tree with aria-hidden and from pointer input with pointer-events: none. The overlay does not trap focus or make the content beneath it inert, so keyboard users can still tab into that content.