Provides core UI components.
At first glance, this module may seem daunting, but its goal is to provide core building blocks for your application, such as tables, forms, and inputs. The components consist mostly of markup and are well-documented with doc strings and declarative assigns. You may customize and style them in any way you want, based on your application growth and needs.
The foundation for styling is Tailwind CSS, a utility-first CSS framework, augmented with daisyUI, a Tailwind CSS plugin that provides UI components and themes. Here are useful references:
daisyUI - a good place to get started and see the available components.
Tailwind CSS - the foundational framework we build on. You will use it for layout, sizing, flexbox, grid, and spacing.
Phoenix.Component - the component system used by Phoenix. Some components, such as
<.link>and<.form>, are defined there.
Summary
Functions
Renders a button with navigation support.
Renders the captcha widget, or nothing when the captcha is disabled.
The "there is nothing here" panel: an icon, a heading and a line of prose.
The grid card every entity list shares — leaderboards, tournaments, groups,
quests. One recipe (bg-base-200, icon in the title, badges stacked
top-right, muted two-line description) so the grids read as one family
instead of four dialects.
An entity's icon: the uploaded icon_url when set, otherwise the typed
default for its entity type (GamendWeb.Icons.default/1) — so every
group, tournament, leaderboard, quest and notification has some icon
without storing one.
A country flag.
Renders flash notices.
Renders a page header: the <h1> in the one style every page title uses
(text-4xl font-black text-base-content/95), an optional subtitle and
actions.
Renders a Heroicon.
Renders an input with label and error messages.
Renders a data list.
Social sign-in buttons for every enabled OAuth provider, preceded by an "or" divider. Renders nothing when no provider is enabled, so the auth forms need no branching of their own.
Renders a pagination bar with Prev/Next buttons, page info, and optional page-size selector.
Renders a table with generic styling.
Renders a stored-UTC timestamp for a human reader.
Translates an error message using gettext.
Translates the errors for a field from a keyword list of errors.
A user's avatar as a round image when they have one (profile_url), falling
back to the generic person icon. Pass class for sizing, e.g. "w-5 h-5".
If the image URL fails to load (provider not ready yet, expired CDN link,
rate-limited avatar CDN), assets/js/avatar_fallback.js hides the broken
image and reveals the same icon. That lives in a real script rather than an
onerror attribute because the CSP here has no script-src 'unsafe-inline',
so an inline handler is refused and the fallback would never fire.
Display label for a user in admin tables: username, then display name, then the
raw id as a last resort. Accepts a loaded %User{}; nil or a not-loaded
association renders "-". Surface the full id separately (e.g. a title
attribute on the cell) so it stays available without cluttering the table.
The game's own line under a player's name — a rank, a title, a guild — read from a metadata path the host configures
The title text user_title/1 renders, or nil. Exposed for plain-text callers.
Functions
Renders a button with navigation support.
Examples
<.button>Send!</.button>
<.button phx-click="go" variant="primary">Send!</.button>
<.button navigate={~p"/"}>Home</.button>Attributes
class(:string)variant(:string) - Must be one of"primary".- Global attributes are accepted. Supports all globals plus:
["href", "navigate", "patch", "method", "download", "name", "value", "disabled"].
Slots
inner_block(required)
Renders the captcha widget, or nothing when the captcha is disabled.
Place it inside the form it guards: the widget writes its token into a hidden
cf-turnstile-response input, which arrives in the phx-submit params at the
top level rather than under the form's as. Verify it with
GamendWeb.UserAuth.verify_captcha/2.
<.form for={@form} phx-submit="save">
<.input field={@form[:email]} type="email" />
<.captcha id="register_captcha" />
<.button>Register</.button>
</.form>id must be unique on the page — the login page renders two forms, and two
widgets sharing an id would leave the second one unrendered.
Attributes
id(:string) (required)
The "there is nothing here" panel: an icon, a heading and a line of prose.
For a page that legitimately has no content — no changelog file, no results,
an empty list — not for an error. Both halves of the copy are attributes, and
both should be translated: the text on the changelog page was an English
literal on every host for exactly as long as it was written inline.
Example
<.empty_state
icon="hero-document-text"
title={gettext("No results.")}
text={gettext("Add a changelog file at CHANGELOG.md to display it here.")}
/>Attributes
icon(:string) (required)title(:string) (required)text(:string) - Defaults tonil.
The grid card every entity list shares — leaderboards, tournaments, groups,
quests. One recipe (bg-base-200, icon in the title, badges stacked
top-right, muted two-line description) so the grids read as one family
instead of four dialects.
<.entity_card
navigate={~p"/leaderboards/#{group.slug}"}
title={group.title}
icon_url={group.icon_url}
type={:leaderboard}
description={group.description}
>
<:badges>
<span class="badge badge-success">{gettext("Active")}</span>
</:badges>
</.entity_card>With navigate the card is a <.link>; without it a <div>, and any
phx-click/title in rest lands on it. class appends to the wrapper —
state borders (border-success), cursor-pointer, and the like.
Attributes
title(:string) (required)icon_url(:string) - Defaults tonil.icon(:atom) - Defaults tonil.type(:atom) (required) - Must be one of:group,:tournament,:leaderboard,:quest, or:notification.description(:string) - Defaults tonil.navigate(:string) - Defaults tonil.class(:any) - Defaults tonil.- Global attributes are accepted.
Slots
badgesinner_block
An entity's icon: the uploaded icon_url when set, otherwise the typed
default for its entity type (GamendWeb.Icons.default/1) — so every
group, tournament, leaderboard, quest and notification has some icon
without storing one.
Pass icon (any GamendWeb.Icons atom — the full heroicons catalog)
to override the type default.
Examples
<.entity_icon icon_url={group.icon_url} type={:group} />
<.entity_icon icon_url={nil} type={:quest} icon={:fire} />Attributes
icon_url(:string) - Defaults tonil.icon(:atom) - Defaults tonil.type(:atom) (required) - Must be one of:group,:tournament,:leaderboard,:quest, or:notification.class(:any) - Defaults to"w-6 h-6".
A country flag.
An <img> rather than a CSS background: the flag-icons plugin inlined all 54
as base64 data URIs in the critical stylesheet, which made it 951 KB — 69% of
it flags, blocking the first paint of every page including the ones showing
none. As files the browser fetches only what is on screen, in parallel, each
cached on its own.
Lazy by default because most of a page's flags sit in a closed dropdown.
But lazy is the wrong default for a flag the reader sees at load — the
navbar's locale button, a heading, a picker's selected value: the preload
scanner skips lazy images, the fetch waits for layout, and the decode is
async, so even a cached flag pops in a frame or two after the text beside
it. That is the "flash" on every refresh. eager opts those few out: the
browser fetches them with the HTML and paints them with the first frame.
Sized in em so the caller still controls it with a text-* class, exactly
as the old .fi/.fis classes did.
No cache-busting query: a page carries up to 114 of these, and a content hash on each costs more in HTML than it saves. The artwork is fixed reference data — replace a flag by replacing the file and its year-long cache expires on its own, which is the one case where that is an acceptable wait.
Attributes
code(:any) (required) - ISO alpha-2 country code, or nil for none.square(:boolean) - 1:1 box instead of 4:3. Defaults tofalse.eager(:boolean) - visible at load: fetch and decode with the page. Defaults tofalse.class(:any) - Defaults tonil.
Renders flash notices.
Examples
<.flash kind={:info} flash={@flash} />
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>Attributes
id(:string) - the optional id of flash container.flash(:map) - the map of flash messages to display. Defaults to%{}.title(:string) - Defaults tonil.kind(:atom) - used for styling and flash lookup. Must be one of:info, or:error.- Global attributes are accepted. the arbitrary HTML attributes to add to the flash container.
Slots
inner_block- the optional inner block that renders the flash message.
Renders a page header: the <h1> in the one style every page title uses
(text-4xl font-black text-base-content/95), an optional subtitle and
actions.
The inner block IS the title text — pass words, an icon, a badge; never
another <h1>. A heading start tag inside an open heading is a parse error
the browser recovers from by closing the outer one first, so the page ended
up with an empty <h1> followed by the real one.
class extends the title (flex items-center gap-3 for an icon,
break-all for an unbreakable string) — it does not replace the size, so
every page stays the same size without each caller restating it.
Attributes
class(:any) - extra classes on the <h1>. Defaults tonil.back(:string) - path to go up to; renders a Back button on the title's own line. Defaults tonil.back_label(:string) - overrides the "Back" wording. Defaults tonil.
Slots
inner_block(required)subtitleactions
Renders a Heroicon.
Heroicons come in three styles – outline, solid, and mini.
By default, the outline style is used, but solid and mini may
be applied by using the -solid and -mini suffix.
You can customize the size and colors of the icons by setting width, height, and background color classes.
Icons are extracted from the deps/heroicons directory and bundled within
your compiled app.css by the shared plugin in apps/gamend_web/assets/vendor/heroicons.js.
Examples
<.icon name="hero-x-mark" />
<.icon name="hero-arrow-path" class="ms-1 size-3 motion-safe:animate-spin" />Attributes
name(:string) (required)class(:string) - Defaults to"size-4".
Renders an input with label and error messages.
A Phoenix.HTML.FormField may be passed as argument,
which is used to retrieve the input name, id, and values.
Otherwise all attributes may be passed explicitly.
Types
This function accepts all HTML input types, considering that:
You may also set
type="select"to render a<select>tagtype="checkbox"is used exclusively to render boolean valuesFor live file uploads, see
Phoenix.Component.live_file_input/1
See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input for more information. Unsupported types, such as hidden and radio, are best written directly in your templates.
Examples
<.input field={@form[:email]} type="email" />
<.input name="my-input" errors={["oh no!"]} />Attributes
id(:any) - Defaults tonil.name(:any)label(:string) - Defaults tonil.value(:any)type(:string) - Defaults to"text". Must be one of"checkbox","color","date","datetime-local","utc-datetime-local","email","file","month","number","password","search","select","tel","text","textarea","time","url", or"week".field(Phoenix.HTML.FormField) - a form field struct retrieved from the form, for example: @form[:email].errors(:list) - Defaults to[].checked(:boolean) - the checked flag for checkbox inputs.prompt(:string) - the prompt for select inputs. Defaults tonil.options(:list) - the options to pass to Phoenix.HTML.Form.options_for_select/2.multiple(:boolean) - the multiple flag for select inputs. Defaults tofalse.class(:string) - the input class to use over defaults. Defaults tonil.error_class(:string) - the input error class to use over defaults. Defaults tonil.- Global attributes are accepted. Supports all globals plus:
["accept", "autocomplete", "capture", "cols", "disabled", "form", "list", "max", "maxlength", "min", "minlength", "multiple", "pattern", "placeholder", "readonly", "required", "rows", "size", "step"].
Renders a data list.
Examples
<.list>
<:item title="Title">{@post.title}</:item>
<:item title="Views">{@post.views}</:item>
</.list>Slots
item(required) - Accepts attributes:title(:string) (required)
Social sign-in buttons for every enabled OAuth provider, preceded by an "or" divider. Renders nothing when no provider is enabled, so the auth forms need no branching of their own.
<.oauth_buttons label={gettext("Log in")} />Attributes
label(:string) (required)
Renders a pagination bar with Prev/Next buttons, page info, and optional page-size selector.
Renders nothing when the list fits on one page: two dead buttons and a "1 / 1" counter are noise on every short list in the app. The size selector survives a one-page list when a smaller size would actually split it — otherwise raising the size until everything fits would hide the only control that undoes it.
Attributes
page— current page number (required)total_pages— total number of pages (required)total_count— total number of items (optional, shown in info text)page_size— current page size (optional, enables size selector when combined withon_page_size)on_prev— event name for previous page (required)on_next— event name for next page (required)on_page_size— event name for page size change (optional, enables size selector)page_sizes— list of page size options (default: [25, 50, 100, 200])class— additional CSS classes for the container
Usage
<.pagination
page={@page}
total_pages={@total_pages}
total_count={@count}
page_size={@page_size}
on_prev="prev_page"
on_next="next_page"
on_page_size="page_size"
/>Attributes
page(:integer) (required)total_pages(:integer) (required)total_count(:integer) - Defaults tonil.page_size(:integer) - Defaults tonil.on_prev(:string) (required)on_next(:string) (required)on_page_size(:string) - Defaults tonil.page_sizes(:list) - Defaults to[25, 50, 100, 200].value(:map) - extra phx-value-* pairs sent with every event, e.g. which section the list belongs to. Defaults to%{}.class(:string) - Defaults tonil.
Renders a table with generic styling.
Examples
<.table id="users" rows={@users}>
<:col :let={user} label="id">{user.id}</:col>
<:col :let={user} label="username">{user.username}</:col>
</.table>Attributes
id(:string) (required)rows(:list) (required)row_id(:any) - the function for generating the row id. Defaults tonil.row_click(:any) - the function for handling phx-click on each row. Defaults tonil.row_item(:any) - the function for mapping each row before calling the :col and :action slots. Defaults to&Function.identity/1.
Slots
col(required) - Accepts attributes:label(:string)
action- the slot for showing user actions in the last table column.
Renders a stored-UTC timestamp for a human reader.
The server has no timezone database and no idea where the reader is, so it
emits the instant in UTC and marks it; local_time.js rewrites the text in
the viewer's own zone and locale once it runs. Without JS the UTC text stands,
which is why it is labelled rather than left to look local.
format is "datetime" (default), "date", "time" or "full".
<.timestamp at={@user.inserted_at} />
<.timestamp at={@message.inserted_at} format="time" class="text-xs" />Attributes
at(:any) (required) - a DateTime, or nil to render the dash.format(:string) - Defaults to"datetime". Must be one of"datetime","date","time", or"full".class(:string) - Defaults tonil.empty(:string) - text shown whenatis nil. Defaults to"-".
Translates an error message using gettext.
Translates the errors for a field from a keyword list of errors.
A user's avatar as a round image when they have one (profile_url), falling
back to the generic person icon. Pass class for sizing, e.g. "w-5 h-5".
If the image URL fails to load (provider not ready yet, expired CDN link,
rate-limited avatar CDN), assets/js/avatar_fallback.js hides the broken
image and reveals the same icon. That lives in a real script rather than an
onerror attribute because the CSP here has no script-src 'unsafe-inline',
so an inline handler is refused and the fallback would never fire.
crossorigin="anonymous" is load-bearing, not decoration: /play and
/game/* are served cross-origin isolated for Godot's SharedArrayBuffer
(see GamendWeb.Plugs.GameHeaders), and under
Cross-Origin-Embedder-Policy: require-corp a cross-origin subresource is
blocked unless it either sends Cross-Origin-Resource-Policy or is fetched in
CORS mode. OAuth avatar CDNs (Google, Discord, Steam, Gravatar) send no CORP
header but do send Access-Control-Allow-Origin: *, so asking for CORS mode
is what makes them load on those pages. Same-origin and object-storage avatars
are unaffected — buckets already need CORS for the presigned upload flow.
Attributes
user(:any) - Defaults tonil.class(:string) - Defaults to"w-6 h-6".
Display label for a user in admin tables: username, then display name, then the
raw id as a last resort. Accepts a loaded %User{}; nil or a not-loaded
association renders "-". Surface the full id separately (e.g. a title
attribute on the cell) so it stays available without cluttering the table.
The game's own line under a player's name — a rank, a title, a guild — read from a metadata path the host configures:
config :gamend_web, :user_title_meta_path, ["player", "rank"]Core knows nothing about what a rank IS; it only knows where the host keeps
it. Renders nothing when the path is unset, the user has no metadata, or the
value is blank, so a fresh account shows a bare name rather than an empty
badge. Takes a %User{}, a metadata map, or nil.
One component so every place that names a player — leaderboards, tournament rosters, party lists, profile cards — says the same thing about them, and a host that changes where the title lives changes one config key.
Attributes
user(:any) (required)class(:string) - Defaults to"text-xs text-base-content/60".
The title text user_title/1 renders, or nil. Exposed for plain-text callers.