Skip to content

Buttons & Actions

This category covers the reusable button and action-bar controls used across the Praxrr UI. All components live under $ui/ (source: packages/praxrr-app/src/lib/client/ui/) and use Svelte 4 export let prop declarations with forwarded on:click handlers. They compose into connected control strips for list pages, toolbars, and dropdown triggers.

Multi-variant button (primary/secondary/danger/ghost) with optional icon, responsive sizing, tooltip, and anchor mode. Renders a <button> normally, or an <a> when href is set. The element is wrapped in a Tooltip.

import Button from '$ui/button/Button.svelte';
Prop Type Default Required Description
text string '' No Button label text; when empty and icon is set, the button renders icon-only (square padding).
variant 'primary' | 'secondary' | 'danger' | 'ghost' 'secondary' No Visual style. primary=accent fill, secondary=bordered neutral, danger=red fill, ghost=transparent.
size 'xs' | 'sm' | 'md' 'sm' No Size preset controlling padding, rounding, text size, and icon size (xs=12, sm=14, md=16).
disabled boolean false No Disables the button (ignored in anchor mode); applies not-allowed cursor and 50% opacity.
icon ComponentType | null null No Lucide/Svelte icon component to render alongside (or instead of) the text.
iconColor string '' No Tailwind class(es) applied to the icon; falls back to a neutral color for the ghost variant.
textColor string '' No Tailwind class(es) applied to the text span; falls back to a neutral color for the ghost variant.
iconPosition 'left' | 'right' 'left' No Whether the icon renders before or after the text/slot.
type 'button' | 'submit' 'button' No Native button type (button mode only).
responsive boolean false No When true, auto-switches to the xs size on screens below 768px via a matchMedia listener.
hideTextOnMobile boolean false No Visually hides the text on mobile (sr-only md:not-sr-only md:inline); it stays available to screen readers.
fullWidth boolean false No Applies w-full to stretch the button to its container width.
href string | undefined undefined No When provided, renders an <a> anchor instead of a <button>.
target string | undefined undefined No Anchor target attribute (anchor mode only).
rel string | undefined undefined No Anchor rel attribute (anchor mode only).
justify 'center' | 'between' 'center' No Content alignment; 'between' is used for dropdown-style triggers (justify-between).
title string '' No Native title attribute, applied only when no tooltip is set.
ariaLabel string '' No aria-label; falls back to tooltip, then to title for icon-only buttons.
tooltip string '' No Tooltip text rendered via the wrapping Tooltip component.
tooltipPosition 'top' | 'bottom' 'bottom' No Position passed to the Tooltip.
<script>
import Button from '$ui/button/Button.svelte';
import { Plus, Filter } from 'lucide-svelte';
function handleAdd() {
// ...
}
</script>
<Button text="Add" variant="primary" icon={Plus} on:click={handleAdd} />
<Button text="Next" variant="secondary" icon={Filter} iconPosition="right" />
<Button text="Docs" variant="ghost" href="/docs" target="_blank" rel="noopener" />

The interactive island below reproduces the variant and size API against standard Tailwind colors.

Clicked 0 times
  • on:click — forwarded from the underlying <button> or <a>.
  • on:keydown — forwarded from the underlying <button> or <a>; used by dropdown triggers such as DropdownSelect to close on Escape.
  • Default slot — rendered between the text and the right-position icon, for custom content such as dropdown chevrons.
  • Variants: primary, secondary, danger, ghost. Sizes: xs, sm, md.
  • Icon-only mode auto-triggers when icon is set and text is empty (uses square iconOnlySizeClasses).
  • Svelte 4-style export let props with a forwarded on:click; wrapped in $ui/tooltip/Tooltip.svelte.
  • Responsive mode registers a (max-width: 767px) matchMedia listener on mount and cleans it up on destroy.
  • iconSize is derived from the effective size.

Bordered icon button sized for action bars, with an optional dropdown region. Fixed 40px height; square by default. Hovering previews the dropdown; a click, Enter, or Space pins it open.

import ActionButton from '$ui/actions/ActionButton.svelte';
Prop Type Default Required Description
icon ComponentType | undefined undefined No Icon component rendered at size 20 inside the button.
iconClass string '' No Extra Tailwind class(es) appended to the icon.
square boolean true No Fixed square size (h-10 w-10); when false, uses h-10 with horizontal padding (px-4).
hasDropdown boolean false No Enables the dropdown slot region and the trigger’s popup ARIA attributes.
dropdownPosition 'left' | 'right' | 'middle' 'left' No Passed through to the dropdown slot as a slot prop for positioning.
disabled boolean false No Disables the button; applies not-allowed cursor and 50% opacity, removes hover styles.
label string — Yes Accessible name. Applied as both aria-label and the native title tooltip on the button.
type 'button' | 'submit' 'button' No Native button type.
variant 'neutral' | 'danger' 'neutral' No Hover style; danger adds a red icon tint on group hover.
popupRole 'menu' | 'listbox' | 'dialog' 'menu' No Value of the trigger’s aria-haspopup. 'dialog' focuses the first tabbable control on keyboard open and disables auto-close on item choice.
<script>
import ActionButton from '$ui/actions/ActionButton.svelte';
import Dropdown from '$ui/dropdown/Dropdown.svelte';
import DropdownItem from '$ui/dropdown/DropdownItem.svelte';
import { Plus, Filter, FileText } from 'lucide-svelte';
function add() {
// ...
}
</script>
<ActionButton icon={Plus} label="Add" on:click={add} />
<ActionButton icon={Filter} label="Filter" hasDropdown={true} dropdownPosition="right">
<svelte:fragment slot="dropdown" let:dropdownPosition let:close>
<Dropdown position={dropdownPosition} role="menu" label="Filter" onClose={close}>
<DropdownItem icon={FileText} label="Option A" />
</Dropdown>
</svelte:fragment>
</ActionButton>
  • on:click — forwarded from the inner <button>.
  • Default slot — rendered inside the button, after the icon.
  • dropdown slot — exposes slot props dropdownPosition, open (boolean), and close (function that closes the dropdown and clears the pinned and hover state); rendered only while open and hasDropdown is true, inside a wrapper div whose id matches the trigger’s aria-controls.
Input Behavior
Hover Opens a preview; closes 100ms after the pointer leaves the wrapper.
Click / Enter / Space Pins the dropdown open; a second activation while pinned closes it. Keyboard activation (event.detail === 0) also moves focus into the dropdown.
ArrowDown on the trigger Pins the dropdown open and moves focus into it.
Escape inside the wrapper Closes the dropdown and returns focus to the trigger. Handled in the capture phase, so it runs before an inner Dropdown’s onClose.
Choosing an item Clicking an element with role menuitem, menuitemradio, or option closes the dropdown after the item’s own handlers run; keyboard choice refocuses the trigger. menuitemcheckbox items and popupRole="dialog" stay open.
Pointer down or focus outside the wrapper Closes the dropdown.
  • Focus target on open: for popupRole="dialog", the first tabbable element; otherwise the first enabled menuitem/menuitemcheckbox/menuitemradio/option, falling back to the first tabbable element.
  • With hasDropdown, the trigger sets aria-haspopup={popupRole}, aria-expanded (open state), and aria-controls (generated dropdown id).
  • label is required and is applied as the trigger’s aria-label and title, so icon-only triggers always have an accessible name.
  • variant: neutral or danger; square vs. padded (square={false}).
  • The 100ms leave delay lets the pointer travel from the trigger into the dropdown.
  • The dropdown appears with a 150ms fade transition.
  • The outer wrapper is a role="group" div handling mouseenter/mouseleave, focusout, item clicks, and capture-phase Escape.

Layout wrapper that groups action items (SearchAction, ActionButton, ViewToggle, etc.) into a single connected control strip with collapsed borders and auto-rounded outer edges.

import ActionsBar from '$ui/actions/ActionsBar.svelte';
Prop Type Default Required Description
className string '' No Extra Tailwind class(es) appended to the bar’s root (e.g. md:justify-start to left-align).
<script>
import ActionsBar from '$ui/actions/ActionsBar.svelte';
import SearchAction from '$ui/actions/SearchAction.svelte';
import ActionButton from '$ui/actions/ActionButton.svelte';
import ViewToggle from '$ui/actions/ViewToggle.svelte';
import { Plus } from 'lucide-svelte';
let view = 'table';
</script>
<ActionsBar className="md:justify-start">
<SearchAction searchStore={search} responsive />
<ActionButton icon={Plus} label="Add" />
<ViewToggle bind:value={view} />
</ActionsBar>
  • Default slot — holds the child action items. Scoped :global styles collapse inner borders (margin-left: -1px), strip inner rounding, and re-apply rounding to the first/last (or only) child’s bordered elements.
  • On mobile the bar is centered and full-width (flex w-full justify-center), switching to auto-width left-aligned on md+ (md:w-auto md:mx-0).
  • The border/rounding orchestration relies on the DOM shape of children, so items must be direct children.

Action-bar control that toggles a data page between 'cards' and 'table' view via an ActionButton dropdown of the two options. The dropdown opens on click, Enter, or Space (hover only previews it); see ActionButton keyboard behavior.

import ViewToggle from '$ui/actions/ViewToggle.svelte';
Prop Type Default Required Description
value ViewMode ('table' | 'cards') 'table' No Currently selected view mode; intended to be bound (bind:value) — dropdown items assign it on click.
position 'left' | 'right' | 'middle' 'right' No Dropdown positioning, forwarded to the inner Dropdown and the ActionButton’s dropdownPosition.
<script>
import ViewToggle from '$ui/actions/ViewToggle.svelte';
import type { ViewMode } from '$lib/client/stores/dataPage';
let view: ViewMode = 'table';
</script>
<ViewToggle bind:value={view} position="right" />
  • No custom dispatched events; state changes are surfaced by mutating the bindable value prop.
  • No slots exposed to consumers (internally composes ActionButton + Dropdown + DropdownItem for Cards/Table).
  • Two options: Cards (LayoutGrid icon) and Table (Table icon); the Eye icon is the trigger, titled and labelled “Choose view”.
  • Built on ActionButton (icon={Eye}, hasDropdown) with a Dropdown labelled “View” containing two DropdownItems with role="menuitemradio".
  • Selection is reflected by each item’s selected state, exposed as aria-checked. Choosing an option closes the dropdown. Use with bind:value to observe changes.

Search input designed for action bars, backed by a SearchStore. Desktop shows an inline bordered input; in responsive mode on mobile it collapses to a trigger button that opens a full-screen search modal. Supports an active-query badge.

import SearchAction from '$ui/actions/SearchAction.svelte';
Prop Type Default Required Description
searchStore SearchStore — Yes Required search store; the component reads $searchStore.query and calls setQuery()/clear().
placeholder string 'Search...' No Input placeholder text (suppressed on desktop while an activeQuery badge is shown).
activeQuery string '' No An already-applied query shown as an accent Badge inside the field; enables Backspace-to-clear.
responsive boolean false No When true, switches to the mobile button+modal presentation on screens below 768px.
hideIcon boolean false No Hides the leading Search icon and adjusts input padding accordingly.
<script>
import SearchAction from '$ui/actions/SearchAction.svelte';
import { createSearchStore } from '$lib/client/stores/search';
const search = createSearchStore();
function runSearch(query) {
// ...
}
function resetFilter() {
// ...
}
</script>
<SearchAction
searchStore={search}
placeholder="Search components..."
responsive
on:submit={(e) => runSearch(e.detail)}
on:clearQuery={resetFilter}
/>
  • on:submit — dispatches the trimmed query string when Enter is pressed with a non-empty query.
  • on:clearQuery — dispatched (void) when Backspace is pressed on an empty input while an activeQuery exists, or when the activeQuery clear button is clicked.
  • No slots.
  • Desktop inline mode vs. mobile modal mode (driven by responsive + viewport).
  • Clear (X) button state varies: clears current query, clears activeQuery, or closes the modal.
  • createEventDispatcher is typed as { submit: string; clearQuery: void }.
  • Registers a (max-width: 767px) matchMedia listener when responsive; closes the modal automatically when leaving mobile width.
  • Keydown handling: Enter submits, Backspace on empty clears activeQuery, Escape closes the modal.
  • Uses Badge ($ui/badge/Badge.svelte) for the active query chip.

Action-bar filter for selecting one or more PCD sources. Renders as inline segmented pill buttons for small source counts, or a labeled dropdown (with active count badge) when there are many sources, on mobile, or when forced. Supports single- and multi-select modes.

import SourceFilterAction from '$ui/actions/SourceFilterAction.svelte';
Prop Type Default Required Description
sources SourceRef[] [] No Available sources; each maps to an option keyed ${source.type}:${source.id} with a Database or Trash2 icon.
selectedKeys SourceFilterKey[] [] No Currently selected source keys; normalized against available options. Bindable — updated before each change dispatch.
selectionMode 'single' | 'multi' 'multi' No single=exactly one source active (radio-like); multi=any subset, with an ‘All sources’ option and a last-source guard.
position 'left' | 'right' | 'middle' 'right' No Dropdown menu position (dropdown mode).
responsive boolean true No When true, forces dropdown mode on screens below 768px via matchMedia.
hideWhenSingle boolean true No When true, the component renders nothing if there is 0 or 1 source option.
pillsThreshold number 5 No Source count at/above which the inline pills collapse into a dropdown.
dropdownOnly boolean false No Forces dropdown presentation regardless of source count or viewport.
label string 'Sources' No Text label on the dropdown trigger button.
ariaLabel string 'Filter by source' No Accessible label applied to the group and trigger button/title.
disabled boolean false No Disables all interaction (toggle, select-all, dropdown open).
active boolean | undefined undefined No Optional override of the computed ‘active’ (filtered) visual state; when undefined it is derived.
<script>
import SourceFilterAction from '$ui/actions/SourceFilterAction.svelte';
let selectedKeys = [];
function applyFilter(selectedSources) {
// ...
}
</script>
<SourceFilterAction
{sources}
bind:selectedKeys
selectionMode="multi"
on:change={(e) => applyFilter(e.detail.selectedSources)}
/>
  • on:change — dispatches SourceFilterChangeDetail { selectedKeys, selectedSources, selectionMode, active } whenever the normalized selection changes.
  • No slots (internally composes Dropdown + DropdownItem in dropdown mode; native pill buttons in inline mode).
  • Inline segmented pills (aria-pressed toggles) vs. dropdown trigger with Filter icon, active count badge (n/total), and ChevronDown.
  • Multi mode adds an ‘All sources’ item; items use role="menuitemcheckbox" in multi mode and role="menuitemradio" in single mode. Single mode closes the dropdown on select and returns focus to the trigger.
  • Uses local types SourceOption and SourceFilterChangeDetail; keys are the template-literal type ${SourceRef['type']}:${number}.
  • normalizeSelection() filters invalid keys and defaults empty multi-selection to all options / empty single-selection to the first option.
  • computeActiveState() marks the filter active when a strict subset is selected.
  • Dropdown uses use:clickOutside and use:focusOutside to close and also closes when the viewport crosses into mobile width; registers a (max-width: 767px) matchMedia listener when responsive.
  • The dropdown trigger sets aria-haspopup="menu", aria-expanded, and aria-controls. Keyboard activation focuses the first menu item; Escape (on the trigger or inside the menu) closes it and refocuses the trigger. Arrow-key navigation comes from Dropdown.