# Skua usage rules
Skua is a headless-first, token-styled component kit for Phoenix LiveView. Every
interactive surface (dropdowns, dialogs, menus, date pickers, tooltips) renders
in the **browser top layer** and is **viewport-aware** (auto flip/shift/clamp), so
nothing is ever clipped by an ancestor's `overflow`. Behavior ships as one hooks
bundle; styling is 100% CSS tokens.
## The one rule that matters
**Never hand-roll a control the browser or a CSS hack would render. Use the Skua
component.** Skua exists so you don't drop a native `<select>`, a CSS modal, or an
absolutely-positioned dropdown into an app — those look unstyled (the OS draws the
open list), break in dark mode, and clip inside scroll containers. Every Skua
overlay is already token-styled and viewport-aware.
| If you're tempted to write… | ❌ Don't | ✅ Use instead |
| --- | --- | --- |
| a dropdown / combobox | `<select>`, `<input list>`, a custom `position:absolute` menu | `<.select …>` |
| a multi-select / tag input | `<select multiple>` | `<.select multiple display="badge" …>` |
| a modal | `<div class="modal">`, a custom backdrop | `<.dialog id=…>` |
| a side panel / sheet | a transformed `<div>` | `<.drawer id=… side=…>` |
| a popover / dropdown panel | `position:absolute` + `z-index` | `<.popover id=…>` |
| an action menu | a custom list of links | `<.menu id=…>` + `<.menu_item>` |
| a tooltip | `title=` or a CSS `::after` | `<.tooltip id=… text=…>` |
| a date / time field | `<input type="date">` / `type="time"` | `<.date_input>` / `<.datetime_input>` |
| a text/email/number field | bare `<input>` | `<.input field=… type=…>` |
| a checkbox / radio / switch | bare `<input type=checkbox>` | `<.toggle type=…>` |
| a slider / range | `<input type="range">` | `<.slider … range?>` |
| a one-segment toggle group | radio buttons styled by hand | `<.segmented options=…>` |
| tabs | a custom show/hide | `<.tabs id=…>` + `<:tab>` |
| a collapsible | a custom toggle | `<.accordion>` / `<:item>` |
| a data table | a bare `<table>` | `<.table>` + `<.pagination>` |
| a toast / flash | a custom banner | route flashes through `<Toast.toaster>` |
If a control you need isn't listed here, build it with bare elements **and** add
the `sk-*` classes / tokens (see "Going outside the rails") — don't ship an
unstyled native control.
## Setup (once — usually done by `mix skua.install`)
```elixir
# lib/my_app_web.ex — inside html_helpers
use Skua
```
```css
/* assets/css/app.css — after the tailwind import */
@import "../../deps/skua/assets/css/skua.css";
@source "../../deps/skua/lib";
```
```js
// assets/js/app.js
import { hooks as skuaHooks } from "skua"
const liveSocket = new LiveSocket("/live", Socket, {
params: { _csrf_token: csrfToken },
hooks: { ...skuaHooks, ...appHooks },
})
```
## Forms — always drive with `field={@form[:x]}`
```heex
<.input field={@form[:email]} type="email" label="Work email" required />
<.input field={@form[:age]} type="number" label="Age" />
<.textarea field={@form[:bio]} label="Bio" rows={4} />
<.toggle type="switch" field={@form[:notify]} label="Email me" />
<.toggle type="checkbox" field={@form[:tos]} label="I agree" />
<.segmented field={@form[:view]} options={["List", "Board", "Calendar"]} label="View" />
<.slider field={@form[:volume]} min={0} max={100} label="Volume" />
<.slider name="price" range value={[20, 80]} min={0} max={100} step={5} label="Price" />
<.otp_input field={@form[:code]} length={6} group={3} />
```
- `<.input>` is **polymorphic** (a drop-in for `CoreComponents.input/1`): `type`
of `text|email|password|number|tel|url|search|checkbox|select|textarea|hidden`
routes to the right Skua control, so generated `phx.gen.auth` / `phx.gen.live`
forms look Skua-styled with no edits.
- Pass `field` (a `Phoenix.HTML.FormField`) — Skua derives id/name/value and shows
changeset errors **only after the field is used**, linked via `aria-describedby`.
- Bare `name`/`value` is the escape hatch for inputs outside a form.
- Checkboxes already emit a hidden `false` companion — don't add your own.
## Selection
```heex
<.select field={@form[:status]} label="Status"
options={[{"Open", "open"}, {"Done", "done"}]} prompt="Choose…" />
<.select field={@form[:teams]} multiple display="badge" searchable creatable
options={@teams} placeholder="Add teams…" />
<.phone field={@form[:phone]} label="Phone" country="US" />
<.date_input field={@form[:due]} label="Due date" />
<.datetime_input field={@form[:starts]} label="Starts at" format="12" />
```
- Options are `{label, value}` (or `{label, value, desc}`) tuples. The Skua select
is a real ARIA combobox (a **hidden `<select>` carries the value**, so
`phx-change` works); the visible list is a top-layer, themed panel — never the
OS dropdown. Full keyboard support (arrows/Home/End/type-ahead/Enter/Escape).
- `<.phone>` writes a canonical E.164 value to a hidden input; validate with
`Skua.Phone.validate_phone/3` in your changeset. Bring-your-own validation —
`mix skua.install --with-phone` wires `ex_phone_number` for full checks.
- `<.date_input>` / `<.datetime_input>` are token-styled calendars in the top
layer — **never** `<input type="date">` (whose picker the OS draws).
## Overlays — all top-layer, all viewport-aware
```heex
<.popover id="invite" pad width="280px">
<:trigger><.button>Invite</.button></:trigger>
<.input name="email" placeholder="name@co.com" />
</.popover>
<.menu id="row-actions" trigger_variant="ghost">
<:trigger>Actions</:trigger>
<.menu_item phx-click="edit">Edit</.menu_item>
<.menu_separator />
<.menu_item danger phx-click="delete">Delete</.menu_item>
</.menu>
<.tooltip id="copy-tip" text="Copy to clipboard">
<.button icon_only aria-label="Copy"><.icon name="hero-clipboard" /></.button>
</.tooltip>
<.dialog id="confirm">
<:title>Delete project?</:title>
<:subtitle>This cannot be undone.</:subtitle>
<p class="sk-p">All data will be removed.</p>
<:footer>
<.button data-sk-close>Cancel</.button>
<.button variant="danger" phx-click="delete">Delete</.button>
</:footer>
</.dialog>
<.drawer id="filters" side="right">
<:title>Filters</:title>
<.input field={@form[:q]} label="Search" />
<:footer><.button variant="primary" data-sk-close>Apply</.button></:footer>
</.drawer>
<.button phx-click={Skua.Components.Overlay.open_dialog("confirm")}>Delete…</.button>
```
- Every overlay needs a **stable DOM `id`** so morphdom never replaces the node
(which would drop top-layer state).
- `dialog`/`drawer` are native `<dialog>` + `showModal()` (inert backdrop, focus
trap, Esc-to-close). Open via `Overlay.open_dialog(id)`, close with a
`[data-sk-close]` button or `Overlay.close_dialog(id)`.
- `popover`/`menu`/`select` flip and shift to stay on-screen and open **beside**
their parent when nested — you never set positions or `z-index`.
- **Put Skua inputs inside overlays too** — a `<.select>` or `<.input>` in a
dialog is fine (the panel renders in the top layer, above the dialog).
## Display, feedback & navigation
```heex
<.card><:title>Plan</:title>…<:footer><.button>Upgrade</.button></:footer></.card>
<.header>Team<:subtitle>Manage access</:subtitle><:actions><.button>Invite</.button></:actions></.header>
<.list><:item title="Email">ada@x.com</:item></.list>
<.badge variant="ok">Live</.badge> <.avatar name="Ada Lovelace" />
<.alert variant="warning" title="Heads up">Trial ends in 3 days.</.alert>
<.accordion id="faq"><:item title="Q?">A.</:item></.accordion>
<.breadcrumb><:crumb navigate={~p"/"}>Home</:crumb><:crumb>Here</:crumb></.breadcrumb>
<.progress value={70} /> <.skeleton variant="text" width="60%" /> <.spinner />
<.tabs id="settings"><:tab label="Profile">…</:tab><:tab label="Billing">…</:tab></.tabs>
<.table id="users" rows={@users} sort={@sort} on_sort="sort">
<:col :let={u} field={:name} label="Name" sortable>{u.name}</:col>
<:action :let={u}><.button variant="ghost">Edit</.button></:action>
<:empty>No users yet.</:empty>
</.table>
<.pagination page={@page} per_page={@per} total={@total} on_page="page"
per_page_options={[10, 25, 50]} on_per_page="per_page" />
```
Toasts: render `<Toast.toaster />` once in your layout and push with
`Skua.Components.Toast.toast(socket, :success, "Saved", title: "Done")`. Flash
messages route through `<Toast.flash_group flash={@flash} />` (the installer wires
this). Auto-dismiss scales with severity.
## Theming — one place, no per-component work
Override the public tokens in your app's `:root` and **everything re-skins** — the
whole point of the system:
```css
/* assets/css/app.css */
:root {
--skua-accent: #4f46e5; /* selected / primary */
--skua-radius: 0.75rem; /* one knob → all rounding */
--skua-font-size: 0.9375rem;/* one knob → the whole type scale */
--skua-space: 4px; /* one knob → all padding/gaps */
}
```
Tokens: `--skua-bg/-bg-elevated/-fg/-fg-muted/-border/-ring/-accent/-accent-fg`
(neutrals) and the **status colors** `--skua-danger/-success/-warning/-info`
(badges, toasts, alerts and the chip soft-fills all derive from these, so retune
one and everything stays in harmony — no per-component colours are hardcoded).
Plus `--skua-radius`, `--skua-font`, `--skua-font-mono`, `--skua-font-size`,
`--skua-icon-size`, `--skua-space`, `--skua-shadow`, and the 3 motion tokens. The
light theme lives under `:root[data-theme="light"]`; ship `<.theme_toggle />`.
Status badges/alerts/toasts: `<.badge variant="success|warning|info|danger">`,
`<.alert variant=…>`, and the toast kinds all read those four tokens. Chips and
badges share `--sk-chip-r` (defaults to `--sk-r-sm`) so their rounding always
matches.
## Going outside the rails (50% Skua is fine)
Skua is scoped to `sk-*` classes + your tokens, so it never fights your own CSS:
- **Re-skin globally** → override `--skua-*` tokens (above).
- **One instance** → every component takes `class`; add your Tailwind:
`<.button class="w-full">Save</.button>`.
- **Mix freely** → use Skua for the hard controls (select, date, dialog, menu,
combobox) and hand-roll the rest with your own markup — they coexist.
- **Opt a region out** → don't use Skua components / `sk-*` classes there.
The only hard rule still applies even when you go custom: **don't ship a bare
native `<select>`/`<dialog>`/`<input type=date>`** — those are the ones the OS
styles and the browser positions. Reach for the Skua component or style your own
fully.
## Conventions
- Every overlay/select needs a stable `id`.
- Don't wrap Skua inputs in your own form-level error display — Skua handles it.
- Skua adds **zero third-party JS** and no runtime CSS framework.