Requires Alpine

Most logic is borrowed from Statamic Peak

1. Add the route

/routes/web.php

use App\\Http\\Controllers\\DynamicToken;

// Dynamic Token route for posting a form with Ajax.
Route::get('/!/DynamicToken/refresh', [DynamicToken::class, 'getRefresh']);

2. Add the controller

/app/Http/Controllers/DynamicToken.php

<?php

namespace App\\Http\\Controllers;

use Illuminate\\Http\\Request;

class DynamicToken extends Controller
{
     /**
     * Get refreshed CSRF token.
     *
     * @return string
     */
    public function getRefresh(Request $request)
    {
        $referer = request()->headers->get('referer');
        $contains = str_contains($referer, request()->getHttpHost());
        if (empty($referer) || !$contains) {
            abort(404);
        }

        return response()->json([
            'csrf_token' => csrf_token()
        ]);
    }
}

3. Add Site/Global JS

import Alpine from 'alpinejs';
window.Alpine = Alpine
Alpine.start()

// Global get CSRF token function (used by forms).
window.getToken = async () => {
    return await fetch('/!/DynamicToken/refresh')
        .then((res) => res.json())
        .then((data) => {
            return data.csrf_token
        })
        .catch(function (error) {
            this.error = 'Something went wrong. Please try again later.'
        })
}

4. Add the JS to the form view

(just add to wherever your form is invoked)

{{# The form script handling validation and submission via AJAX with `fetch()`. #}}
 <script>
    document.addEventListener('alpine:initializing', () => {
        Alpine.data('sending', () => {
            return {
                error: false,
                errors: [],
                sending: false,
                success: false,
                sendForm: async function() {
                    this.sending = true

                    // Post the form.
                    fetch(this.$refs.form.action, {
                        headers: {
                            'X-Requested-With' : 'XMLHttpRequest',
                            'X-CSRF-Token' : await getToken(),
                        },
                        method: 'POST',
                        body: new FormData(this.$refs.form)})
                    .then(res => res.json())
                    .then(json => {
                        if (json['success']) {
                            this.errors = []
                            this.success = true
                            this.error = false
                            this.sending = false
                            this.$refs.form.reset()

                            setTimeout(function(){
                                this.success = false
                            }, 4500)
                        }
                        if (json['error']) {
                            this.sending = false
                            this.error = true
                            this.success = false
                            this.errors = json['error']
                        }
                    })
                    .catch(err => {
                        err.text().then( errorMessage => {
                            this.sending = false
                        })
                    })
                }
            }
        })
    })
</script>
{{# Create the selected form and reference Alpine data in `sending()`. Prevent form from submitting with POST as we will submit with AJAX. #}}
<div x-data="sending">
    {{ form:create in="{ form:handle }"
        id="form-{form:handle}"
        class="flex flex-wrap"
        x-ref="form"
        @submit.prevent="sendForm()"
        js="alpine:dynamic_form"
    }}
</div>

5. Handle the Errors

On the parent to show the error message

<div x-data="{ id: $id('error-label') }" class="flex flex-col space-y-1">
    {{ field }}
    {{# Display error label when there is a validation error with the name field. #}}
    <template x-if="errors.{{ handle }}">
        <div class="block text-sm text-red-600" :id="id" x-text="errors.{{ handle}}"></div>test
    </template>
    {{# Instructions Below #}}
    {{ if instructions && instructions_position == "below" }}
    <div class="text-sm font-normal prose">{{ instructions | markdown }}</div>
    {{ /if }}
</div>

And on the field itself to show the error border state

{{# Bind custom error classses when there is a validation error with the field. #}}
<!-- /vendor/statamic/forms/fields/text.antlers.html -->
<input
    class="w-full rounded border-neutral focus-visible:border-primary focus-visible:ring focus-visible:ring-primary motion-safe:transition caret-primary"
    :class="{ '!border-red-600 !focus-visible:border-red-600': errors.{{ handle }} }"
    id="{{ handle }}"
    name="{{ handle }}"
    type="{{ input_type ?? 'text' }}"
    placeholder="{{ placeholder }}"
    :aria-invalid="errors.hasOwnProperty('{{ handle }}')"
    :aria-describedBy="id"
    {{ if character_limit }}maxlength="{{ character_limit }}"{{ /if }}
    {{ old ?= 'value="{old}"' }}
    {{ js_attributes }}
/>