Components

Living documentation for the Malphas design system. See them all working on a single page

Actions

Button

Button with color variants, sizes and pass-through attributes (HTMX, Alpine).

Modal

Overlay dialog that is truly modal: focus is trapped inside, the page behind neither scrolls nor is reachable by a screen reader, and on close focus returns to whatever opened it.

Alert Dialog

Modal that asks the user to confirm a destructive or irreversible action. role=alertdialog with the description announced, initial focus on Cancel, and clicking outside does not count as an answer. ConfirmAttrs takes hx-delete and similar attributes.

Link

Text link with color variants. Href goes through templ's URL sanitizer, and external links get rel=noopener.

Drawer

Modal panel that slides in from the side or from the bottom (Side: "bottom"). Same primitive as Modal — trapped focus, locked scroll, inert content behind. A side panel that lets Tab escape is even worse than a dialog that does, because the page behind still looks usable.

Dropdown

Menu button pattern, with role=menu and arrow-key navigation. It is the opposite of the Drawer: it does not trap focus — Escape returns to the trigger, but Tab leaves and closes it, as a menu should.

Popover

Button that opens a non-modal panel with free-form content. Focus moves into the panel, Escape closes it and returns focus, and tabbing out closes it without trapping anything. Inside a Modal, Escape closes only the popover.

Context Menu

Right-click action menu, opened at the click point and pushed back inside the viewport near the edges. The area takes focus and opens the same menu with Shift+F10 or the Menu key.

Swap

Switches between two visuals. It is a real button with aria-pressed — a toggle whose state assistive technology cannot see is just a drawing.

Data display

Card

Bordered, shadowed container that groups a block of content.

Item

Row with media, title, description and actions. With Href the whole row is clickable, while the actions stay outside the link (the stretched link pattern). ItemGroup builds the list.

Hover Card

Preview of what sits behind a link, on mouse hover or keyboard focus, with a delay so it does not flicker. It is only a complement: the trigger is a real link, and nothing should live only in the card.

Bubble

Chat bubble with variants, aligned to the start or the end. Consecutive messages from the same author flatten the corners where they meet; reactions expose their meaning and count to screen readers, not just the emoji.

Marker

System line in a conversation, not written by any participant: someone joined, a reply is being generated, a date separator. Live updates use role=status; the divider's label stays readable to screen readers.

Message

Message with avatar, name, time and a Bubble. In consecutive messages from the same author, only the first shows the header, but every message keeps author and time in its accessible name.

Attachment

File or image in a composer, a message or an upload list, with its state shown as text (uploading, processing, error, done), not only as a progress bar. The name only becomes a link once the file is ready; the actions stay reachable, especially on error. AttachmentGroup scrolls horizontally.

Message Scroller

Conversation transcript that behaves like a chat: it follows new messages only if you are at the bottom, shows "Jump to latest" (with how many arrived) when you have scrolled up, and keeps your position when older messages are added at the top. It does not care how messages arrive: it watches its own content. Uses role=log with aria-live=polite.

Typography

Styling for long-form text whose HTML comes from elsewhere (Markdown, help pages, terms): headings, lists, quotes and code, using the Malphas tokens. Also available as the prose-malphas class.

Badge

Compact status or count label, solid or outlined.

Kbd

Visual representation of a key, using the semantic <kbd> element.

Avatar

Round or square profile picture, falling back to initials, with an optional presence indicator.

Stat

Block that highlights a number with a title and context. It uses dt/dd, so it must sit inside a <dl>.

Table

Table for dense data, with its own horizontal scroll. Zebra striping and density are set once on the table and reach the cells through a descendant selector.

Data Table

Table with sorting, search and pagination done on the server. Everything is a real link or GET field (it works without JS); with JS, each action swaps only the table region and the URL keeps the state. The search box sits outside the swapped region, so typing is never interrupted.

Carousel

Horizontal slides with scroll-snap. Fully navigable from the keyboard, with the position announced on each slide, and scrolling respects prefers-reduced-motion.

Collapse

Expandable area. The trigger is a real button — a clickable div does not take focus, does not respond to Enter or Space, and exposes no role to screen readers.

Accordion

Group of expandable panels, with an optional exclusive mode where opening one closes the others.

Timeline

Chronological list of events, with a rail connecting the markers.

Countdown

Countdown timer. The initial value comes from the server, so the clock is right from the first paint. aria-live is off on purpose: a number announced every second makes the page unusable with a screen reader.

Tooltip

Contextual hint. It opens on hover and also on focus, closes with Escape, and exposes its text through aria-describedby; a hint that exists only visually never reaches screen reader users.

Data input

Input

Text field with a label associated by id. Accepts any native type.

Select

Single choice built on the native <select> — the second deliberate exception to Alpine, after Range. It gets the native picker on mobile, type-ahead and full keyboard support; in exchange, the open options cannot be styled.

Combobox

Searchable select, for lists too long to scroll. Focus stays in the text field and the highlighted option is announced through aria-activedescendant; a hidden input carries the value, so free text is never submitted.

Date Picker

The native <input type="date">, styled — the same choice as Select: the platform picker (the wheel on a phone, the grid on desktop) is better at touch, keyboard and screen readers, and the value always arrives as YYYY-MM-DD. For a calendar that stays visible on the page, use Calendar.

Calendar

A month grid for picking a day or a range, always visible on the page. The server builds the month; changing months fetches the adjacent month and swaps only the calendar. APG keyboard pattern; month and weekday names, and the first day of the week, follow the locale.

Questionnaire

Multi-step form, one question at a time (single choice, multiple choice, free text, skippable). Each step is a POST and the server decides what comes next: validation, conditional questions and resuming all live in Go. Keys 1–9 choose, Enter advances; Back never validates.

Label

Field label with an optional required marker. The asterisk is aria-hidden: the field's own required attribute is what announces it as required.

Field

Brings together label, control, description and a server-side error. ControlAttrs() returns the aria-describedby and aria-invalid the control needs to carry.

Textarea

Multi-line text field. With MaxLength it gets a counter that counts the way the browser's maxlength does (an emoji counts as 2), already filled in on the server render.

Input Group

Field with an attached prefix and suffix — currency, domain, a button. The focus ring wraps the whole group.

Toggle Button

Two-state button with aria-pressed, like bold in an editor. Not the same as Toggle, which is a settings switch. With Name, it submits its value only while pressed.

Toggle Group

A set of ToggleButtons with a single Tab stop. In single mode at most one is pressed (clicking it again releases it); in multiple mode each toggles independently. Arrow keys move focus only.

OTP

Verification code. The first field carries autocomplete=one-time-code, which is what makes iOS and Android offer the SMS code right on the keyboard.

Toggle

Sliding switch. The visual is Alpine; underneath sits a real sr-only checkbox, so it submits with a form as usual.

Checkbox

Checkbox with a third state. Indeterminate only exists as a JS property — there is no HTML attribute — so it is applied on init and announced with aria-checked=mixed.

Radio

Single choice. Navigate with the arrows: they move focus and selection together, as in a native radio group, and skip disabled options.

Rating

Star rating, navigable with the arrows. In read-only mode it becomes output: no focus, nothing submitted, a single text label.

Range

Numeric slider. Like Select and Date Picker, it keeps the native control visible: reimplementing dragging, touch and pointer capture would be a lot of work to end up worse than what the browser already does. Alpine only mirrors the value into the label.

Navigation

Navbar

Top navigation bar. It knows how to open a Drawer on mobile but does not require one: without a DrawerID, no menu button is rendered.

Menubar

Application menu bar (File, Edit, View). One Tab stop for the whole bar; ← → move between menus and carry the open menu along, ↑ ↓ move inside it, Escape returns to the top level and Tab closes everything.

Sidebar

Application frame: grouped side navigation with the content next to it. It collapses to icons only (labels stay available to screen readers and as a title tooltip) and remembers the choice; on a phone it becomes a Menu button that opens the same navigation in a modal Drawer.

Command

Command palette: a modal that searches grouped actions and pages, opened with Ctrl+K / ⌘K. Focus stays in the search box and the arrows move through the results (aria-activedescendant); the search starts empty every time it opens.

Pagination

Navigation between pages, collapsing long ranges. The current page is a span with aria-current, not a link — linking to where you already are is a dead control.

Menu

Vertical navigation. It deliberately does not use role=menu — that role is for application menus, and applying it to site links makes screen readers announce the page as a program menu. For the menu button pattern, use Dropdown.

Tabs

Tabs following the WAI-ARIA pattern: Tab reaches the strip once, then the arrows navigate, Home and End go to the ends, and disabled tabs are skipped.

Breadcrumbs

Navigation trail. The last item is never a link and gets aria-current=page.

Steps

Step-by-step progress indicator. The current step gets aria-current=step, since the state is otherwise shown only by color.

Feedback

Layout & utilities