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
Skeleton
Animated placeholder shown while data loads. It is aria-hidden: it stands for content that does not exist yet.
Loading
Loading indicator as a spinner, dots or bars. Announced with role=status and a text label.
Progress
Linear progress bar built on the native <progress> element, which comes with accessibility built in.
Alert
Message banner for info, success, warning or error. Always announced with role=alert.
Toast
Temporary floating notification, pinned to the bottom-right corner. It goes away on its own after 3 seconds.
Empty
Empty state with icon, title, description and action. The title is a paragraph: the heading level depends on the page, which the component does not know.
Layout & utilities
Divider
Horizontal or vertical rule, with optional text in the middle. Without a label it is decorative and hidden from screen readers.
Aspect Ratio
Reserves a media element's aspect ratio before it loads, so the page does not jump. An unknown ratio falls back to 16/9 instead of collapsing the box.
Scroll Area
Native scrolling with a themed scrollbar. It takes focus (tabindex=0) so it can be scrolled from the keyboard, and therefore requires a Label.
Resizable
Two panels with a divider you drag or move from the keyboard (arrows, Shift for 1%, Home/End), following the APG window splitter pattern. The server renders the initial size; with an ID, the last size is remembered in this browser.
Footer
Structural footer with link columns and a note line. It keeps the contentinfo landmark even when nested (explicit role), and passes its links to Link so they inherit href sanitizing.
Hero
Highlight section for the top of a page. It handles the framing — spacing, centering and max width — and leaves the content to you.
Indicator
Pins a badge to the corner of any element — counters, status dots, "new" tags.
Join
Groups buttons or inputs into a single visual block, squaring the inner corners and overlapping adjacent borders.