Skip to content
fayeuiStatic / 0.1.0
LLMs
Components / Static edition

Buttons and actions

Variants, intent, sizing, pending states, composition and toggles.

Quick start

After the application setup, copy this component into your application. It imports only public packages; the complete live demo later on this page also uses documentation layout helpers.

'use client';
import { useState } from 'react';
import { Button } from '@fayegram-ai/faye-ui/controls/button';
import { Chip } from '@fayegram-ai/faye-ui/controls/chip';
import { Inline } from '@fayegram-ai/faye-ui/layout/layout';

export function FavoriteAction() {
  const [favorite, setFavorite] = useState(false);
  return <Inline wrap>
    <Button onClick={() => setFavorite(value => !value)}>
      {favorite ? 'Remove from favorites' : 'Save to favorites'}
    </Button>
    <Chip selected={favorite} onClick={() => setFavorite(value => !value)}>Favorite</Chip>
    <Button disabled>Unavailable</Button>
  </Inline>;
}

API reference: Button, Chip.

Usage contract

Native button semantics are retained. A pending action is disabled against duplicate activation. Provide a name for icon-only controls. A link-shaped button should render an anchor with a real href, not emulate navigation with click handlers.

Customization

Use semantic tokens for visual changes, public component parts for structural customization, and explicit props for instance behavior. The surrounding site lets you compare light and dark, different brands and density.

Source and verification

The examples below import actual public package entries. Full source is downloadable. Source coverage does not establish keyboard, hydration or browser compatibility; those require the application test suite.

Geometry and pending state

Long button labels wrap within the available width, including with enlarged text. Leading and trailing icons keep their size alongside the label; icon-only buttons remain square. Toggle groups keep a single scrollable row when space is limited, with keyboard focus bringing the active control into view. Set orientation="vertical" for a column and matching up/down keyboard navigation.

The outer border box uses the shared control heights: small 32/28px, medium 36/32px, large 44/36px in comfortable/compact density. Coarse-pointer buttons have at least a 44px target. Pass pending={busy} in both states. The default pendingPosition="center" keeps the idle label centered, then replaces it with a spinner while retaining the accessible name and dimensions. Use pendingPosition="start" or "end" to keep the label visible and reserve an indicator slot on that side. Pending actions expose aria-busy without the faded unavailable appearance. IconButton also defaults to the centered presentation. pendingIndicator accepts custom progress artwork. Use a native submit button inside a form for Enter/pointer parity and an explicit reset/cancel policy for drafts.

Content spacing and icon alignment

Horizontal inset is 4/8/8/12px for xs/sm/md/lg; icon-to-label gap is 4/4/6/8px. These insets sit inside the existing outer-height contract. IconButton has zero horizontal padding at every size, keeping its icon centered inside the square border. Icons and labels share a centered flex row. Use startIcon and endIcon for decorative artwork before and after the label; these slots are hidden from assistive technology by default, so keep the accessible action name in the label. Both slots are configurable component parts (start-icon and end-icon) and follow logical direction. Supply tightly bounded SVG artwork at the intended icon size; extra whitespace inside a custom SVG viewBox remains part of the artwork. Pending labels retain their dimensions.

Grouped and chip actions

ButtonGroup supports horizontal and vertical orientation, with shared size, tone, variant and disabled defaults. Explicit child props win. For a split button, put a primary Button and a Menu.Trigger rendered as an IconButton inside the group; Menu continues to own focus and dismissal.

Chip is passive by default. onClick creates a native primary button; href creates a link. selected supplies pressed semantics for an action and current-page semantics for a link. onRemove adds a separate labeled button. Disabled chips disable both controls. Leading/trailing artwork, soft/outline presentation, sm/md sizes and wrapping policy are supported. Keep the label text meaningful and give each remove action a specific removeLabel.

Use startIcon and endIcon separately or together. Both slots are decorative, use the same size-dependent gap and remain centered in RTL. For a removable chip that can be restored inline, reserve the footprint of both states so adjacent filters do not move. The live example uses the same Chip frame for removal and restoration.

Live examples

Try the controls below, then read or copy the complete example to adapt it for your application.

Variants and intent

Actions activated: 0

Icons before and after labels

Sizes, loading and grouped actions

Composed anchorTheme documentation

Persistent toggles and removable chips

DesignRead-only labelFeedback guide
Alignment: start
Read the complete example
'use client';
import { Plus, ArrowRight, RotateCcw } from '@lucide/icons';
import { useEffect, useRef, useState } from 'react';
import { Button, IconButton, ButtonGroup } from '@fayegram-ai/faye-ui/controls/button';
import { Link } from '@fayegram-ai/faye-ui/controls/link';
import { Toggle } from '@fayegram-ai/faye-ui/controls/toggle';
import { ToggleGroup } from '@fayegram-ai/faye-ui/controls/toggle-group';
import { Chip } from '@fayegram-ai/faye-ui/controls/chip';
import { Inline, Stack } from '@fayegram-ai/faye-ui/layout/layout';
import { Icon } from '@fayegram-ai/faye-ui/core/icon';
import { Section, Result } from '../example';
export default function Buttons() {
    const designRef = useRef<HTMLSpanElement>(null), restoreRef = useRef<HTMLSpanElement>(null), moveChipFocus = useRef(false);
    const [count, setCount] = useState(0), [pending, setPending] = useState(false), [chip, setChip] = useState(true), [favorite, setFavorite] = useState(false), [alignment, setAlignment] = useState<string[]>(['start']);
    useEffect(() => {
        if (moveChipFocus.current) {
            (chip ? designRef : restoreRef).current?.querySelector('button')?.focus({preventScroll:true});
            moveChipFocus.current = false;
        }
    }, [chip]);
    return <><Section title="Variants and intent"><Stack gap="3">{(['solid', 'outline', 'soft', 'ghost'] as const).map(variant => <Inline key={variant} wrap>{(['neutral', 'accent', 'secondary', 'danger', 'success', 'warning', 'info'] as const).map(tone => <Button key={tone} variant={variant} tone={tone} onClick={() => setCount(n => n + 1)}>{tone}</Button>)}</Inline>)}<Result>Actions activated: {count}</Result></Stack></Section><Section title="Icons before and after labels"><Stack gap="3">{(['xs','sm','md','lg'] as const).map(size=><Inline key={size} wrap><Button size={size} startIcon={<Icon icon={Plus} size="sm"/>} data-testid={`leading-${size}`} onClick={()=>setCount(n=>n+1)}><span data-icon-action-label>New project</span></Button><Button size={size} endIcon={<Icon icon={ArrowRight} size="sm" mirrorInRtl/>} data-testid={`trailing-${size}`} onClick={()=>setCount(n=>n+1)}><span data-icon-action-label>Continue</span></Button><Button size={size} startIcon={<Icon icon={Plus} size="sm"/>} endIcon={<Icon icon={ArrowRight} size="sm" mirrorInRtl/>} data-testid={`both-${size}`} onClick={()=>setCount(n=>n+1)}>Create and open</Button></Inline>)}</Stack></Section><Section title="Sizes, loading and grouped actions"><Inline wrap>{(['xs', 'sm', 'md', 'lg'] as const).map(size => <Button size={size} key={size}>{size}</Button>)}<Button disabled>Disabled</Button><Button pending={pending}>Pending example</Button><Button variant="ghost" onClick={() => setPending(p => !p)}>Toggle pending</Button><IconButton aria-label="Add item" onClick={() => setCount(n => n + 1)}><Icon icon={Plus}/></IconButton></Inline><ButtonGroup aria-label="History"><Button onClick={() => setCount(n => n - 1)}>Undo</Button><Button onClick={() => setCount(n => n + 1)}>Redo</Button></ButtonGroup><Button nativeButton={false} render={<a href="/docs/customization/"/>}>Composed anchor</Button><Link href="/docs/theming/">Theme documentation</Link></Section><Section title="Persistent toggles and removable chips"><ToggleGroup value={alignment} onValueChange={setAlignment} aria-label="Alignment"><Toggle value="start" aria-label="Align start">Start</Toggle><Toggle value="center" aria-label="Align center">Center</Toggle><Toggle value="end" aria-label="Align end">End</Toggle></ToggleGroup><Inline wrap data-chip-actions><span className="docs-chip-slot"><span style={{visibility:chip?undefined:"hidden"}} aria-hidden={!chip || undefined} inert={!chip || undefined}><Chip ref={designRef} tone="accent" onRemove={() => {moveChipFocus.current = true; setChip(false);}} removeLabel="Remove design filter">Design</Chip></span><span style={{visibility:chip?"hidden":undefined}} aria-hidden={chip || undefined} inert={chip || undefined}><Chip ref={restoreRef} onClick={() => {moveChipFocus.current = true; setChip(true);}} leading={<Icon icon={RotateCcw} size="xs"/>}>Restore chip</Chip></span></span><Chip>Read-only label</Chip><Chip selected={favorite} onClick={() => setFavorite(value => !value)}>Favorites</Chip><Chip href="/docs/feedback/">Feedback guide</Chip></Inline><Result>Alignment: {alignment.join(', ') || 'none'}</Result></Section></>;
}