alouette
Version:
A modern, customizable design system built on top of NativeWind v5 with configurable defaults
224 lines (162 loc) • 6.68 kB
Markdown
---
name: alouette-actions
description: >
Buttons (Button, IconButton, ExternalLinkButton, InternalLinkButton) and
pressable surfaces (PressableBox, PressableListItem). variant is
contained|outlined|ghost, size is sm|md, accent defaults to brand. Button
label is the required text prop (not children); IconButton
requires aria-label. Interactive hover/focus/active/disabled states are built
in. For async onPress use ActionButton (runs the promise, shows spinner +
inline error); Button state ('loading'|'success'|'failed') is the manual
escape hatch that overlays a spinner/terminal icon and disables the button.
Load when adding buttons or custom pressable elements.
type: core
library: alouette
library_version: "22.0.0"
requires:
- alouette-theming
sources:
- "christophehurpeau/alouette:packages/alouette/src/ui/actions/Button.tsx"
- "christophehurpeau/alouette:packages/alouette/src/ui/actions/ActionButton.tsx"
- "christophehurpeau/alouette:packages/alouette/src/ui/actions/IconButton.tsx"
- "christophehurpeau/alouette:packages/alouette/src/ui/actions/PressableBox.tsx"
- "christophehurpeau/alouette:packages/alouette/src/ui/actions/PressableListItem.tsx"
---
This skill builds on alouette-theming. Read it first for the accent model.
# alouette — Actions
Buttons and pressables carry interactive token states (hover/focus/active/
disabled) automatically. `variant` is `"contained" | "outlined" | "ghost"`;
`size` is `"sm" | "md"`; `accent` defaults to `"brand"`.
## Setup
```tsx
import { Button } from "alouette";
import { CheckRegularIcon } from "alouette-icons/phosphor-icons/CheckRegularIcon";
<Button text="Save" icon={<CheckRegularIcon />} onPress={save} />;
```
## Core Patterns
### Button variants and accents
```tsx
<Button text="Save" /> {/* contained, brand */}
<Button variant="outlined" text="Cancel" />
<Button variant="ghost" text="Dismiss" />
<Button accent="danger" text="Delete" />
<Button size="sm" text="Small" />
```
### Async action — prefer ActionButton
For a direct action whose `onPress` is async (delete, retry, publish — not a
form submit, which uses `FormSubmitButton`), use `ActionButton`. It runs the
promise, derives the button `state` for you (spinner while pending, terminal
icon on settle), and renders an inline `ErrorMessage` on rejection. Map the
error to a string via the required `errorToMessage`.
```tsx
import { ActionButton } from "alouette";
<ActionButton
accent="danger"
text="Delete"
onPress={async () => deleteItem(id)}
errorToMessage={(error) => (error instanceof Error ? error.message : "Failed")}
/>;
```
`ActionButton` omits `onPress`/`state` from `ButtonProps` and manages them
itself — don't pass `state` to it.
### Manual loading state
Reach for `Button`'s `state` prop directly only when `ActionButton` doesn't fit
(state driven externally, e.g. by a form or store). `state` drives a transient
overlay and disables the button: `"loading"` shows an indeterminate spinner,
`"success"` a check, `"failed"` a warning icon (the spinner plays its finish
animation before the terminal icon). Any non-`undefined` `state` disables presses.
```tsx
import { Button, type ButtonState } from "alouette";
<Button text="Save" state={submitState} onPress={save} />
// submitState: ButtonState | undefined = "loading" | "success" | "failed"
```
Don't drive loading UI yourself (spinner + manual `disabled`) — use
`ActionButton`, or set `state`.
### Icon-only button
```tsx
import { IconButton } from "alouette";
import { XRegularIcon } from "alouette-icons/phosphor-icons/XRegularIcon";
<IconButton icon={<XRegularIcon />} aria-label="Close" onPress={close} />;
```
`size` is `"sm" | "md"` or a number (custom diameter in px); `iconSize="fill"`
makes the icon take 80% of the button.
### Pressable surfaces
`PressableBox` is a themed, pressable container (`variant`, `accent`,
`forceStyle`). `PressableListItem` is a row with a trailing caret.
```tsx
import { PressableBox, PressableListItem, Text } from "alouette";
<PressableBox onPress={open}>
<Text>Custom card</Text>
</PressableBox>
<PressableListItem onPress={open}>
<Text>Row label</Text>
</PressableListItem>;
```
### Link buttons
```tsx
import { ExternalLinkButton, InternalLinkButton } from "alouette";
<ExternalLinkButton href="https://example.com" text="Open" />
<InternalLinkButton href="/settings" text="Settings" />
```
For full external-link control (in-app browser vs new tab), see
alouette-external-links/SKILL.md.
## Common Mistakes
### HIGH Passing the button label as children instead of text
Wrong:
```tsx
<Button>Save</Button>
```
Correct:
```tsx
<Button text="Save" />
```
`Button` renders its label from the required `text` prop (plus an optional `icon`
prop); children are ignored, so `<Button>Save</Button>` shows no label.
Source: packages/alouette/src/ui/actions/Button.tsx
### HIGH Using non-existent variant names
Wrong:
```tsx
<Button variant="ghost-contained" text="Cancel" />
<Button variant="primary" text="Save" />
```
Correct:
```tsx
<Button variant="ghost" text="Cancel" />
<Button accent="brand" text="Save" />
```
`variant` is only `"contained" | "outlined" | "ghost"`; the accent is chosen via
the `accent` prop. (`ghost` is a variant value, not a separate boolean prop.)
Source: packages/alouette/src/ui/actions/PressableBox.tsx, ui/actions/Button.tsx
### MEDIUM IconButton without aria-label
Wrong:
```tsx
<IconButton icon={<XRegularIcon />} onPress={close} />
```
Correct:
```tsx
<IconButton icon={<XRegularIcon />} aria-label="Close" onPress={close} />
```
`IconButton` types `aria-label` as required because it has no text label;
omitting it is an accessibility failure and a type error.
Source: packages/alouette/src/ui/actions/IconButton.tsx
### HIGH IconButton triggering an async action instead of ActionButton
Wrong:
```tsx
<IconButton icon={<TrashRegularIcon />} aria-label="Delete" onPress={async () => deleteItem(id)} />
```
Correct:
```tsx
<ActionButton
accent="danger"
text="Delete"
icon={<TrashRegularIcon />}
onPress={async () => deleteItem(id)}
errorToMessage={(error) => (error instanceof Error ? error.message : "Failed")}
/>
```
An icon alone doesn't communicate intent reliably — a visible text label is
required for any action with consequences. An `IconButton` also has no async
affordance: it won't show a spinner while the promise is pending or surface a
rejection. Use `ActionButton` (text + optional `icon`) so the intent is explicit
and the pending/error states are handled.
Source: packages/alouette/src/ui/actions/ActionButton.tsx, ui/actions/IconButton.tsx