Skip to content

Alpine.js Forms

Alpine.js is a lightweight frontend library that offers reactive binding and declarative syntax without the complexity of larger frameworks like React or Vue. It is a popular choice for static sites, Jekyll, Astro, or WordPress projects.

Using Alpine.js, you can build contact forms that submit data asynchronously via AJAX, preventing page redirects and providing instant user feedback.


If you are building with an AI assistant (such as Cursor, Claude, v0, Windsurf, or ChatGPT), copy and paste this prompt to generate an Alpine.js form:

Create a responsive HTML contact form using Alpine.js for interactive state.
Submission Endpoint:
https://api.lucidforms.co/f/{your-form-id}
Requirements:
- Use Alpine x-data with reactive fields: Name, Email, and Message
- Include an invisible honeypot trap field (_honeypot) for spam bot protection
- Use @submit.prevent with fetch() to send JSON to the submission endpoint asynchronously
- Include 'Content-Type: application/json' and 'Accept: application/json' headers
- Manage UI states: idle, loading (disable button), success banner, and error alert
- Reset form data upon successful submission
- Style with modern, responsive Tailwind CSS classes

Here is a copy-pasteable example of an Alpine.js form. It handles input binding, displays status alerts, and submits values using fetch().

<!-- Load Alpine.js from CDN (if not already included in your project) -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js" defer></script>
<div
x-data="{
fields: {
name: '',
email: '',
message: '',
_honeypot: ''
},
status: 'idle', // 'idle' | 'loading' | 'success' | 'error'
errorMessage: '',
async submitForm() {
this.status = 'loading';
this.errorMessage = '';
// Honeypot spam bot check
if (this.fields._honeypot) {
this.status = 'success';
this.resetForm();
return;
}
try {
const response = await fetch('https://api.lucidforms.co/f/{your-form-id}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(this.fields)
});
const data = await response.json();
if (response.ok) {
this.status = 'success';
this.resetForm();
} else {
this.status = 'error';
this.errorMessage = data.error || 'Form submission failed. Please try again.';
}
} catch (err) {
this.status = 'error';
this.errorMessage = 'A network error occurred. Please check your connection.';
}
},
resetForm() {
this.fields.name = '';
this.fields.email = '';
this.fields.message = '';
this.fields._honeypot = '';
}
}"
style="max-width: 400px; margin: 0 auto; font-family: sans-serif;"
>
<h2>Contact Us</h2>
<!-- Success Message -->
<template x-if="status === 'success'">
<div style="color: green; background-color: #e8f5e9; padding: 10px; border-radius: 4px; margin-bottom: 15px;">
✓ Thank you! Your message was submitted successfully.
</div>
</template>
<!-- Error Message -->
<template x-if="status === 'error'">
<div style="color: red; background-color: #ffebee; padding: 10px; border-radius: 4px; margin-bottom: 15px;">
✗ <span x-text="errorMessage"></span>
</div>
</template>
<form x-on:submit.prevent="submitForm">
<!-- Honeypot bot trap (hidden) -->
<input type="text" x-model="fields._honeypot" style="display: none;" tabindex="-1" autocomplete="off" />
<div style="margin-bottom: 12px;">
<label for="name" style="display: block; margin-bottom: 4px; font-weight: 500;">Name</label>
<input
type="text"
id="name"
x-model="fields.name"
required
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;"
/>
</div>
<div style="margin-bottom: 12px;">
<label for="email" style="display: block; margin-bottom: 4px; font-weight: 500;">Email</label>
<input
type="email"
id="email"
x-model="fields.email"
required
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;"
/>
</div>
<div style="margin-bottom: 16px;">
<label for="message" style="display: block; margin-bottom: 4px; font-weight: 500;">Message</label>
<textarea
id="message"
x-model="fields.message"
required
rows="4"
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;"
></textarea>
</div>
<button
type="submit"
x-bind:disabled="status === 'loading'"
style="padding: 10px 16px; background-color: #4f46e5; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;"
>
<span x-text="status === 'loading' ? 'Sending...' : 'Submit'"></span>
</button>
</form>
</div>

  • x-data: Initializes the component’s state variables (inputs, status, error states) and custom logic methods (like submitForm and resetForm).
  • x-model: Enables two-way data binding. As the user types in the input fields, Alpine automatically syncs the value to your javascript x-data object properties.
  • x-on:submit.prevent: Attaches an event listener to the form’s submit action. The .prevent modifier automatically stops the browser’s default reload event, allowing Alpine’s submitForm() method to run in the background.
  • x-if: Conditionally renders UI chunks (like success or error cards) based on the component’s current status variable.
  • x-text: Dynamically binds the inner text of an HTML tag to state values, such as displaying the custom error message returned from the API.