---
title: Cushion
published: 2026-10-11
lastmod: 2026-10-11
url: https://larasell.dev/docs/packages/cushion
---

## Table of contents
- [Installation](#installation)
- [Defining a form](#defining-a-form)
- [Registering the form](#registering-the-form)
- [Wiring up Inertia](#wiring-up-inertia)
- [Default values](#default-values)
- [Clearing a draft](#clearing-a-draft)
- [Transferring a guest draft on login](#transferring-a-guest-draft-on-login)

# Cushion

Cushion helps you auto-save forms. This prevents users from re-typing a whole form in case they
accidentally close the tab or browser. And it lets customers continue their checkout later
without them needing to keep the tab open.

## Installation

To use Cushion you need to install a Composer and an npm package:

```bash
composer require larasell-dev/cushion
npm i @larasell-dev/cushion
```

## Defining a form

Extend a `Form`{href="https://raw.githubusercontent.com/Larasell-dev/cushion/refs/heads/main/src/Form.php"} class and define the fields with Laravel's validation rules:

```php
use App\Models\User;
use Larasell\Cushion\Form;

final class ContactForm extends Form
{
    public function fields(): array
    {
        return [
            'first_name' => 'string|max:255|required',
            'email' => 'email|max:255|required',
            'message' => 'string|required',
        ];
    }
}
```

The `required` rules only apply on submit; draft saves treat every field as
optional.

## Registering the form

Now, you need to register the form in your `web.php` file:

```php
Form::add(ContactForm::class);
```

Some forms should only be accessible by authenticated users. To make
sure this is the case you can add a middleware to a form:

```php
Form::add(CheckoutForm::class)
    ->middleware('auth');
```

## Wiring up Inertia

Use the `useFormDraft` hook to persist edits as the user types.

```ts
import { useFormDraft } from '@larasell-dev/cushion';

function MyForm() {
    const { track } = useFormDraft(draft, {
        url: '/forms/contact/draft',
    });

    return <Input type="text" name="first_name" {...track('first_name')} />;
}
```

Use the `track` method for [uncontrolled](https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components) inputs. And the
`set` method for controlled ones:

```ts
function MyForm() {
    const [firstName, setFirstName] = useState(draft.first_name);

    const { set } = useFormDraft(draft, {
        url: '/forms/contact/draft',
    });

    return (
        <Input
            type="text"
            name="first_name"
            value={firstName}
            onChange={(event) => {
                setFirstName(event.target.value);
                set('first_name', event.target.value);
            }}
        />
    );
}
```

## Default values

Sometimes, it's useful to pre-fill a form with data you already have access to, like
filling out the email and name of a user, or re-using the address from a previous order:

```php
final class CheckoutForm extends Form
{
    // ...

    public function defaults(?User $user): array
    {
        if ($user === null) {
            return [];
        }

        return [
            'first_name' => str($user->name)->beforeLast(' ')->toString(),
            'last_name' => str($user->name)->afterLast(' ')->toString(),
            'email' => $user->email,
        ];
    }
}
```

Pass any arguments the form needs for the defaults, e.g. the authenticated user.
The `hydrate` method merges the default values and the stored draft values and returns them:

```php
public function show(Request $request): Response
{
    return Inertia::render('checkout', [
        'billing' => CheckoutForm::get()->hydrate($request->user()),
    ]);
}
```

A stored draft value overwrites any default value. So if a user changed a field
with a default value and then reloads the page, they'll see the stored draft.

## Clearing a draft

There are two ways to clear a draft and each one is useful in a different scenario.

1. If you need a "Reset" button then you can use the `clear` method of the `useFormDraft` hook:

```ts
function MyForm() {
    const { clear } = useFormDraft(draft, {
        url: '/forms/contact/draft',
    });

    return <Button onClick={clear}>Start over</Button>;
}
```

`clear` first drops pending edits and aborts an in-flight save,
then sends a request to the form's draft route.

2. On the server you can remove the draft yourself, e.g. after handling a
   submission:

```php
ContactForm::get()->clearDraft();
```

## Transferring a guest draft on login

Drafts for guests are scoped to the session. This is fine, but when
the user logs in or creates a new account they'll lose the form drafts.

To prevent this you can transfer the drafts from a guest to an authenticated
user:

```php
// Get the form from the current session,
// must be called before logging the user in
$guestForm = ContactForm::get();

$request->authenticate();
$request->session()->regenerate();

$guestForm->transfer($request->user());
```

This will overwrite an existing draft of the same form, if the
user already has a draft. Nothing happens if the user does not have a draft.
