Actions
Action components are where the user triggers something: click, pick from a list, confirm. They carry intent — a click changes data, navigates, or starts a flow. That's why this category groups both the direct trigger (Button) and the elements around an action: contextual hint (Tooltip), a set of secondary actions (DropdownMenu), an anchored panel (Popover), and the safeguard before something destructive (ConfirmDialog).
Reach for this page when you need the user to do something. For data entry (text, selection, dates) see inputs; to present collections, see data.
Button
When to use: the primary or secondary action of any screen — submit a form, open a modal, navigate. The default action trigger.
Primary button with variants, sizes, and a loading state.
import { Button } from "tempest-react-sdk";
import { Plus, Trash } from "lucide-react";
<Button>Save</Button>;
<Button variant="danger" leftIcon={<Trash size={16} />}>
Delete
</Button>;
<Button variant="outline" loading>
Loading…
</Button>;
<Button variant="link" rightIcon={<ArrowRight size={14} />}>
See more
</Button>;
<Button iconOnly aria-label="Add">
<Plus size={16} />
</Button>;
<Button fullWidth pill>
CTA
</Button>;
| Prop | Type | Default |
|---|---|---|
variant |
"primary" \| "secondary" \| "success" \| "danger" \| "soft" \| "outline" \| "ghost" \| "link" |
"primary" |
size |
"xs" \| "sm" \| "md" \| "lg" \| "xl" |
"md" |
loading |
boolean |
false |
fullWidth |
boolean |
false |
iconOnly |
boolean (square, requires aria-label) |
false |
pill |
boolean (pill border-radius) |
false |
leftIcon |
ReactNode |
— |
rightIcon |
ReactNode |
— |
iconOnly needs an accessible label
iconOnly removes the visible text, so screen readers have nothing to announce. Always pass aria-label describing the action (aria-label="Delete"). Without it the button is a mute icon to assistive tech.
loading blocks double-clicks
loading disables the button and sets aria-busy="true" — it's the standard for async submits. Turn it on the moment you fire the request to avoid duplicate requests from repeated clicks.
FloatingActionButton
When to use: the primary, persistent action of a screen (create, compose, add) that should always be reachable, floating over the content. Round when icon-only, or extended (pill) when given a
label.
By default it is fixed to the bottom-right corner; pass position="none" to place it inline (e.g. inside a NavigationRail). Spreads all native <button> props (onClick, disabled, etc.).
import { FloatingActionButton } from "tempest-react-sdk";
import { Plus } from "lucide-react";
<FloatingActionButton icon={<Plus />} aria-label="New" position="none" onClick={create} />;
<FloatingActionButton icon={<Plus />} label="New order" onClick={create} />;
| Prop | Type | Default |
|---|---|---|
icon |
ReactNode |
— |
label |
ReactNode (present → extended FAB) |
— |
position |
"bottom-right" \| "bottom-left" \| "none" |
"bottom-right" |
size |
"sm" \| "md" \| "lg" |
"md" |
variant |
"primary" \| "surface" |
"primary" |
| ... | All HTMLButtonElement attributes |
— |
An icon-only FAB needs aria-label
Without a visible label, the round FAB has no accessible name. Always pass aria-label describing the action (aria-label="New"); when a label is present, it already serves as the name.
Tooltip
When to use: give extra context to a control whose meaning isn't obvious — typically
iconOnlybuttons. Never for critical information.
A portaled hover tooltip. Shows on hover and on keyboard focus.
<Tooltip content="Delete permanently" placement="bottom" openDelay={300}>
<Button variant="danger" iconOnly aria-label="Delete">
<Trash />
</Button>
</Tooltip>
| Prop | Type | Default |
|---|---|---|
content |
ReactNode |
— |
placement |
"top" \| "right" \| "bottom" \| "left" |
"top" |
openDelay |
number (ms before showing) |
150 |
disabled |
boolean (turn off, trigger unchanged) |
false |
Don't hide essential information in a tooltip
Touch users have no hover — they'll never see the content. A tooltip is reinforcement, not the only source of information needed to complete the task.
DropdownMenu
When to use: group secondary actions behind a single trigger ("More actions", a profile menu) when they don't fit the main bar.
A dropdown menu of actions. Keyboard nav (↑↓ Home End Esc). Each entry needs a stable id (used as the React key).
<DropdownMenu
trigger={<Button variant="ghost">More actions</Button>}
items={[
{ type: "label", id: "h", label: "Account" },
{ type: "item", id: "edit", label: "Edit profile", onSelect: () => navigate("/profile") },
{ type: "separator", id: "s1" },
{ type: "item", id: "logout", label: "Sign out", onSelect: logout, danger: true },
]}
/>
| Entry type | Fields |
|---|---|
"item" |
id, label, icon?, onSelect, disabled?, danger? |
"label" |
id, label |
"separator" |
id |
Component props: trigger (ReactElement), items (DropdownMenuEntry[]), placement ("bottom-start" \| "bottom-end" \| "top-start" \| "top-end", default "bottom-start").
Closes after selecting
Selecting an item fires onSelect and closes the menu. For a panel that stays open with multiple choices (checkboxes, filters), use Popover instead of DropdownMenu.
Popover
When to use: a floating panel with arbitrary content (filters, a mini-form, a preview) anchored to a trigger — when you need more than a list of actions.
A generic floating panel (anchor + outside-click + Esc dismiss). Works controlled (open + onOpenChange) or uncontrolled (defaultOpen).
<Popover
open={open}
onOpenChange={setOpen}
placement="bottom"
trigger={<Button>Filters</Button>}
>
<Stack gap={3}>
<Checkbox label="Active only" />
<Checkbox label="Paid" />
<Button onClick={() => setOpen(false)}>Apply</Button>
</Stack>
</Popover>
| Prop | Type | Default |
|---|---|---|
trigger |
ReactElement (cloned with handlers) |
— |
open |
boolean |
— (controlled) |
onOpenChange |
(open: boolean) => void |
— |
defaultOpen |
boolean (uncontrolled usage) |
false |
placement |
"top" \| "bottom" \| "left" \| "right" |
"bottom" |
closeOnEsc |
boolean |
true |
closeOnOutsideClick |
boolean |
true |
No collision detection
Popover doesn't reposition automatically when it hits the viewport edge. If you need automatic flip/shift, prefer DropdownMenu (simple list) or integrate Floating UI in your app.
ConfirmDialog
When to use: the last barrier before an irreversible or costly action (delete, overwrite, cancel). Always with
variant="danger"when destructive.
A pre-built destructive prompt on top of Modal (text + 2 buttons).
<ConfirmDialog
open={open}
title="Delete user"
description={`This action is permanent. Delete ${user.name}?`}
confirmLabel="Yes, delete"
cancelLabel="Cancel"
variant="danger"
loading={deleting}
onConfirm={async () => {
await deleteUser(user.id);
setOpen(false);
}}
onCancel={() => setOpen(false)}
/>
| Prop | Type | Default |
|---|---|---|
open |
boolean |
— |
title |
ReactNode |
— |
description |
ReactNode |
— |
confirmLabel |
string |
"Confirmar" |
cancelLabel |
string |
"Cancelar" |
variant |
"primary" \| "danger" |
"primary" |
loading |
boolean (shows spinner + disables both) |
false |
onConfirm |
() => void \| Promise<void> |
— |
onCancel |
() => void |
— |
Control loading during the request
onConfirm accepts a promise, but ConfirmDialog doesn't manage the loading state itself — pass loading={deleting} driven by your own state to lock both buttons while the async action runs.
Default labels are Portuguese
confirmLabel/cancelLabel default to "Confirmar"/"Cancelar". Pass explicit English strings in EN-locale apps.
InstallButton
PWA install button wired to the beforeinstallprompt event (useBeforeInstallPrompt). Renders null when the app can't be installed — no prompt captured yet, already installed, or running standalone — so you can drop it in without guarding visibility. Inherits every Button prop.
import { InstallButton } from "tempest-react-sdk";
import { Download } from "lucide-react";
<InstallButton variant="primary" leftIcon={<Download size={18} />} />;
| Prop | Type | Default |
|---|---|---|
label |
ReactNode |
"Instalar app" |
onResult |
(o: "accepted" \| "dismissed" \| "unsupported") => void |
— |
| … | all Button props (variant, size, leftIcon) |
— |
InstallBanner
Dismissible bottom banner inviting the user to install the PWA. Shows only when a prompt was captured and the app is not already standalone; on platforms that never fire beforeinstallprompt (iOS Safari) it stays hidden — surface manual instructions elsewhere. storageKey remembers the dismissal across reloads.
<InstallBanner
title="Install the app"
description="Offline access and a home-screen shortcut."
storageKey="my-app:install-dismissed"
/>;
| Prop | Type | Default |
|---|---|---|
title |
ReactNode |
"Instale o app" |
description |
ReactNode |
— |
installLabel |
string |
"Instalar" |
dismissLabel |
string |
"Dispensar" |
icon |
ReactNode |
— |
storageKey |
string |
— (session) |
onResult |
(o) => void |
— |
Recap
| Component | Use for | Trigger |
|---|---|---|
Button |
Fire the primary/secondary action | click |
FloatingActionButton |
Floating, persistent primary action | click |
InstallButton |
Install the PWA (hides when not applicable) | click |
InstallBanner |
Dismissible invite to install the PWA | click |
Tooltip |
Non-critical context on a control | hover/focus |
DropdownMenu |
A list of secondary actions (closes on pick) | click |
Popover |
A floating panel with arbitrary content | click |
ConfirmDialog |
Confirm a destructive action before running it | — |
Key accessibility points:
- Destructive actions should use
variant="danger". Button.loadingis the standard for async submits — it blocks double-clicks.- Tooltips should not contain critical information (touch users don't see hover).
iconOnlyrequiresaria-label.
Related: overlay (ConfirmDialog is built on Modal) · inputs (data entry) · feedback (toasts/alerts after the action).