# Buttons

URL: https://docs.forms.saastro.io/docs/buttons
> Configure submit, next, and back buttons for your forms.

# Buttons

`<Form>` automatically renders up to three navigation buttons:

| Button   | Purpose                 | When Shown                |
| -------- | ----------------------- | ------------------------- |
| `submit` | Submit the form         | On the last step          |
| `next`   | Go to next step         | On non-final steps        |
| `back`   | Return to previous step | When there's step history |

Their click behavior is managed by the form: `back` returns to the previous step, `next` validates the current step and advances, and `submit` validates and submits. All three are disabled while a submission is in flight.

---

## Default Buttons

You don't have to configure anything — when `.buttons()` is omitted (or only partially set), each button falls back to these defaults:

| Button   | Label       | Variant     |
| -------- | ----------- | ----------- |
| `submit` | `'Submit'`  | `'default'` |
| `next`   | `'Next →'`  | `'outline'` |
| `back`   | `'← Back'`  | `'ghost'`   |

The default labels come from the package's built-in English strings (while a submission is in flight, the submit button additionally shows `'Submitting...'`). There are three ways to override the labels:

1. **Per form** — pass `label` in `.buttons()` (shown throughout this page).
2. **Per locale** — provide an i18n overlay; see the [i18n guide](/docs/i18n).
3. **Globally** — swap all package defaults at once with `setDefaultMessages` (Spanish apps: `setDefaultMessages(es)`); see the [i18n guide](/docs/i18n).

```tsx
// i18n overlay: translate the default buttons per locale
const config = {
  ...baseConfig, // a FormConfig, e.g. from FormBuilder.create(...).build()
  locale: 'es',
  i18n: {
    translations: {
      es: {
        buttons: {
          submit: { label: 'Enviar' },
          next: { label: 'Siguiente →' },
          back: { label: '← Atrás' },
        },
      },
    },
  },
};
```

---

## Basic Configuration

```tsx
const config = FormBuilder.create('wizard')
  .addField('name', (f) => f.type('text').label('Name').required())
  .addField('email', (f) => f.type('email').label('Email').email().required())
  .addStep('step1', ['name'])
  .addStep('step2', ['email'])
  .buttons({
    submit: {
      type: 'submit',
      label: 'Send',
      variant: 'default',
    },
    next: {
      type: 'next',
      label: 'Continue',
      variant: 'default',
    },
    back: {
      type: 'back',
      label: 'Back',
      variant: 'outline',
    },
  })
  .build();
```

> Every field built with `FieldBuilder` needs a schema (the only exception is `hidden` fields) — a validation method such as `.required()`, `.optional()`, or an explicit `.schema()` provides one. Without it, `addField` throws immediately.

---

## Button Properties

### Properties applied to navigation buttons

| Property  | Type                                                                       | Default                          | Description                          |
| --------- | -------------------------------------------------------------------------- | -------------------------------- | ------------------------------------ |
| `type`    | `'submit' \| 'next' \| 'back'`                                              | -                                | Button type (required)               |
| `label`   | `string`                                                                    | English defaults (see above)     | Button text                          |
| `variant` | `'default' \| 'destructive' \| 'outline' \| 'secondary' \| 'ghost' \| 'link'` | `'default'`/`'outline'`/`'ghost'` per button | Visual style, forwarded to your `Button` component |
| `size`    | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'`                                      | `'default'` is forwarded when unset | Button size, forwarded to your `Button` component |
| `icon`    | `ReactNode`                                                                 | -                                | Icon element (placement is automatic) |

Icon placement on navigation buttons is fixed: the `back` icon renders **before** the label; `next`/`submit` icons render **after** the label. `iconPosition` has no effect on them.

### Properties NOT applied to navigation buttons

`ButtonConfig` also declares `iconPosition`, `action`, `loading`, `disabled`, `effect`, and `rounded`, but the navigation buttons rendered by `<Form>` ignore them:

- Click handling, disabled state, and loading state are managed by the form itself.
- `iconPosition` (default `'left'`), `action`, and `loading` **do** work when a button is added as a form field — see [Custom Button Actions](#custom-button-actions).
- `effect` and `rounded` are declared in the type but **not yet implemented**: no render path forwards them to the rendered button, even if your injected `Button` component supports them.

### Variants

The `variant` string is forwarded to the `Button` component you inject, so the exact look depends on your component. With a shadcn/ui-style `Button`:

```tsx
variant: 'default'; // Primary solid background
variant: 'secondary'; // Secondary muted background
variant: 'outline'; // Border only
variant: 'ghost'; // No background, subtle hover
variant: 'destructive'; // Red/danger style
variant: 'link'; // Looks like a link
```

### Sizes

`size` is forwarded as-is to your `Button` component:

```tsx
size: 'xs'; // Extra small
size: 'sm'; // Small
size: 'md'; // Medium
size: 'lg'; // Large
size: 'xl'; // Extra large
```

When `size` is unset, the literal string `'default'` is forwarded — shadcn/ui-style `Button` components treat that as their standard size.

---

## Button Icons

Add icons to buttons using React elements. Placement on navigation buttons is automatic:

```tsx

.buttons({
  submit: {
    type: 'submit',
    label: 'Send Message',
    icon: <Send className="h-4 w-4" />, // rendered after the label
  },
  next: {
    type: 'next',
    label: 'Continue',
    icon: <ArrowRight className="h-4 w-4" />, // rendered after the label
  },
  back: {
    type: 'back',
    label: 'Go Back',
    icon: <ArrowLeft className="h-4 w-4" />, // rendered before the label
  },
})
```

---

## Button Alignment

Control horizontal alignment of the button container with `align` (default: `'end'`):

```tsx
.buttons({
  align: 'between',
  submit: { type: 'submit', label: 'Send' },
})
```

| Value          | Behavior                                              |
| -------------- | ----------------------------------------------------- |
| `'start'`      | Left aligned                                          |
| `'center'`     | Center aligned                                        |
| `'end'`        | Right aligned (default)                               |
| `'between'`    | Space between (back left, next/submit right)          |
| `'responsive'` | Stacked on mobile, right-aligned row on desktop       |

Passing any other value makes `.buttons()` throw with `[@saastro/forms] Invalid button align value: ...`.

### Visual Examples

```
align: 'start'
[Back] [Submit]

align: 'center'
              [Back] [Submit]

align: 'end'
                              [Back] [Submit]

align: 'between'
[Back]                              [Submit]

align: 'responsive' (mobile)
[Back]
[Submit]

align: 'responsive' (desktop)
                              [Back] [Submit]
```

---

## Inline Buttons

Place buttons on the same row as fields (useful for search forms):

```tsx
const config = FormBuilder.create('search')
  .layout('manual')
  .columns(12)
  .addField('query', (f) =>
    f
      .type('text')
      .label('Search')
      .hideLabel()
      .placeholder('Search...')
      .columns({ default: 10 })
      .optional(),
  )
  .addStep('main', ['query'])
  .buttons({
    inline: true, // Same row as fields
    submit: {
      type: 'submit',
      label: 'Search',
    },
  })
  .build();
```

---

## Multi-Step Form Example

Complete example with navigation buttons:

```tsx

const config = FormBuilder.create('onboarding')
  .layout('manual')
  .columns(12)

  // Step 1: Personal Info
  .addField('name', (f) =>
    f.type('text').label('Full Name').required().columns({ default: 12, md: 6 }),
  )
  .addField('email', (f) =>
    f.type('email').label('Email').email().required().columns({ default: 12, md: 6 }),
  )

  // Step 2: Preferences
  .addField('theme', (f) =>
    f
      .type('select')
      .label('Theme')
      .options([
        { value: 'light', label: 'Light' },
        { value: 'dark', label: 'Dark' },
        { value: 'system', label: 'System' },
      ])
      .required(),
  )

  // Step 3: Confirmation
  .addField('terms', (f) =>
    // .mustBeTrue() (not .required()) is what forces a checkbox to be checked
    f.type('checkbox').label('I accept the terms and conditions').mustBeTrue(),
  )

  // Define steps
  .addStep('personal', ['name', 'email'])
  .addStep('preferences', ['theme'])
  .addStep('confirmation', ['terms'])
  .initialStep('personal')

  // Configure buttons
  .buttons({
    align: 'between',
    submit: {
      type: 'submit',
      label: 'Complete',
      icon: <Check className="h-4 w-4" />,
      variant: 'default',
    },
    next: {
      type: 'next',
      label: 'Continue',
      icon: <ArrowRight className="h-4 w-4" />,
      variant: 'default',
    },
    back: {
      type: 'back',
      label: 'Previous',
      icon: <ArrowLeft className="h-4 w-4" />,
      variant: 'outline',
    },
  })

  .build();
```

---

## Custom Button Actions

The navigation buttons' click behavior is fixed — an `action` set in `.buttons()` is ignored by `<Form>`. To render a button with a custom action, add it as a **field** instead (`ButtonConfig` is part of the field union). Use `.addFields()` with a raw config, since `action` is a function and has no dedicated builder method:

```tsx
const config = FormBuilder.create('with-cancel')
  .addField('email', (f) => f.type('email').label('Email').email().required())
  .addFields({
    cancel: {
      type: 'button',
      label: 'Cancel',
      variant: 'ghost',
      action: () => {
        if (confirm('Are you sure you want to cancel?')) {
          window.location.href = '/';
        }
      },
    },
  })
  .addStep('main', ['email', 'cancel'])
  .build();
```

Button fields honor `action`, `iconPosition` (default `'left'`), and `loading` (shows a loading state and disables the button), in addition to `label`, `variant`, `size`, and `icon`. Unlike navigation buttons, a button field with no `size` set receives `'md'` (not the literal `'default'`).

---

## API Reference

### FormButtons Interface

```tsx
interface FormButtons {
  submit?: ButtonConfig;
  next?: ButtonConfig;
  back?: ButtonConfig;
  align?: 'start' | 'center' | 'end' | 'between' | 'responsive';
  inline?: boolean;
}
```

### ButtonConfig Interface

```tsx
interface ButtonConfig extends BaseFieldProps {
  type: 'button' | 'submit' | 'next' | 'back';
  label?: string;
  action?: () => void; // button fields only — ignored on navigation buttons
  loading?: boolean; // button fields only — ignored on navigation buttons
  icon?: ReactNode;
  iconPosition?: 'left' | 'right'; // button fields only (default 'left');
  // navigation buttons place icons automatically
  variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
  effect?: string; // declared but not yet implemented — never forwarded
  rounded?: string; // declared but not yet implemented — never forwarded
}
```

---

## Related

- [Multi-Step Forms](/docs/multi-step) — Step navigation, conditional routing
- [i18n](/docs/i18n) — Translating the default button labels per locale
