Overview
This guide demonstrates how to integrate SpamBlock with Tailwind CSS forms, including styled success/error messages, loading states, and smooth animations. Tailwind's utility classes make it easy to create professional form feedback.
Prerequisites
- Tailwind CSS configured in your project
- Basic knowledge of Tailwind utility classes
- SpamBlock pixel script included
Setup
1. Include SpamBlock Pixel
Add the SpamBlock script to your page:
<script src="https://api.spamblock.io/sdk/pixel/v1.js defer"></script>
2. Basic Tailwind Form
Here's a contact form with Tailwind styling:
<form
data-block-spam
action="/api/contact"
method="post"
id="contact-form"
class="max-w-2xl mx-auto space-y-6">
<div>
<label for="email" class="block text-sm font-medium text-slate-700 mb-2">
Email Address
</label>
<input
type="email"
id="email"
name="email"
required
placeholder="[email protected]"
class="w-full px-4 py-2 border border-slate-300 rounded-lg shadow-sm
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
transition-colors">
</div>
<div>
<label for="message" class="block text-sm font-medium text-slate-700 mb-2">
Message
</label>
<textarea
id="message"
name="message"
rows="5"
required
placeholder="Your message here..."
class="w-full px-4 py-2 border border-slate-300 rounded-lg shadow-sm
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
transition-colors resize-none"></textarea>
</div>
<button
type="submit"
id="submit-btn"
class="w-full bg-blue-600 text-white py-3 px-6 rounded-lg
hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500
focus:ring-offset-2 font-medium transition-colors disabled:opacity-50
disabled:cursor-not-allowed flex items-center justify-center gap-2">
<svg id="spinner" class="hidden animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span id="btn-text">Send Message</span>
</button>
<!-- Status messages (hidden by default) -->
<div id="success-message" class="hidden rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
<div class="flex items-center gap-2">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
<span id="success-text"></span>
</div>
</div>
<div id="error-message" class="hidden rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700">
<div class="flex items-center gap-2">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
<span id="error-text"></span>
</div>
</div>
</form>
3. Event Handling with Tailwind Styling
Add this JavaScript to handle SpamBlock events and show Tailwind-styled feedback:
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('contact-form');
const submitBtn = document.getElementById('submit-btn');
const btnText = document.getElementById('btn-text');
const spinner = document.getElementById('spinner');
const successAlert = document.getElementById('success-message');
const successText = document.getElementById('success-text');
const errorAlert = document.getElementById('error-message');
const errorText = document.getElementById('error-text');
// Hide all alerts
const hideAlerts = () => {
successAlert.classList.add('hidden');
errorAlert.classList.add('hidden');
};
// Show/hide loading state
const setLoading = (loading) => {
if (loading) {
submitBtn.disabled = true;
spinner.classList.remove('hidden');
btnText.textContent = 'Sending...';
} else {
submitBtn.disabled = false;
spinner.classList.add('hidden');
btnText.textContent = 'Send Message';
}
};
// Handle successful submission
form.addEventListener('spamblock:allowed', (event) => {
setLoading(false);
hideAlerts();
const response = event.detail.response;
// Show success message if submission was allowed
if (response && response.allow === true && response.score < 60) {
successText.textContent = 'Thank you! Your message has been sent successfully.';
successAlert.classList.remove('hidden');
// Add fade-in animation
successAlert.classList.add('animate-fade-in');
// Reset form after successful submission
form.reset();
// Hide success message after 5 seconds
setTimeout(() => {
successAlert.classList.add('hidden');
successAlert.classList.remove('animate-fade-in');
}, 5000);
}
});
// Handle blocked submission
form.addEventListener('spamblock:blocked', (event) => {
setLoading(false);
hideAlerts();
const response = event.detail.response;
const reasons = response?.reasons || [];
// Build error message from reasons
let errorMsg = 'Your submission was blocked. ';
if (reasons.includes('disposable_domain')) {
errorMsg += 'Please use a valid email address (disposable domains are not allowed).';
} else if (reasons.includes('profanity_detected')) {
errorMsg += 'Please remove inappropriate language from your message.';
} else if (reasons.includes('honeypot_filled')) {
errorMsg += 'Automated submission detected.';
} else {
errorMsg += `Spam detected (score: ${response?.score || 0}/60).`;
}
errorText.textContent = errorMsg;
errorAlert.classList.remove('hidden');
errorAlert.classList.add('animate-fade-in');
});
// Show loading state when form is submitted
form.addEventListener('submit', () => {
hideAlerts();
setLoading(true);
});
});
Advanced Examples
With Tailwind Animations
Add custom animations to your tailwind.config.js:
module.exports = {
theme: {
extend: {
keyframes: {
'fade-in': {
'0%': { opacity: '0', transform: 'translateY(-10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
'slide-in': {
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(0)' },
},
},
animation: {
'fade-in': 'fade-in 0.3s ease-out',
'slide-in': 'slide-in 0.3s ease-out',
},
},
},
}
Then use them in your HTML:
<div id="success-message" class="hidden rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700 animate-fade-in">
<!-- ... -->
</div>
Toast-Style Notifications
Create toast notifications that slide in from the top:
<!-- Toast Container (fixed position) -->
<div id="toast-container" class="fixed top-4 right-4 z-50 space-y-2">
<!-- Toasts will be inserted here -->
</div>
const showToast = (message, type = 'success') => {
const container = document.getElementById('toast-container');
const toastId = `toast-${Date.now()}`;
const bgColor = type === 'success' ? 'bg-green-500' : 'bg-red-500';
const icon = type === 'success'
? '<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>'
: '<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>';
const toast = document.createElement('div');
toast.id = toastId;
toast.className = `${bgColor} text-white px-6 py-4 rounded-lg shadow-lg flex items-center gap-3 animate-slide-in min-w-[300px]`;
toast.innerHTML = `
<svg class="h-5 w-5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
${icon}
</svg>
<span class="flex-1">${message}</span>
<button onclick="document.getElementById('${toastId}').remove()" class="text-white hover:text-gray-200">
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
`;
container.appendChild(toast);
// Auto-remove after 5 seconds
setTimeout(() => {
toast.classList.add('animate-fade-out');
setTimeout(() => toast.remove(), 300);
}, 5000);
};
// Use in event handlers
form.addEventListener('spamblock:allowed', (event) => {
setLoading(false);
showToast('✅ Message sent successfully!', 'success');
form.reset();
});
form.addEventListener('spamblock:blocked', (event) => {
setLoading(false);
const response = event.detail.response;
showToast(`❌ Submission blocked: ${response?.reasons?.join(', ') || 'Spam detected'}`, 'error');
});
JavaScript-Based Submission
For SPAs or when you need custom submission handling, intercept the spamblock:allowed event to handle submission via fetch():
<form data-block-spam id="contact-form" class="max-w-2xl mx-auto space-y-6">
<!-- Form fields -->
<button type="submit" id="submit-btn" class="w-full bg-blue-600 text-white py-3 px-6 rounded-lg">
<svg id="spinner" class="hidden animate-spin h-5 w-5 text-white inline mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span id="btn-text">Send Message</span>
</button>
<div id="success-message" class="hidden rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700"></div>
<div id="error-message" class="hidden rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700"></div>
</form>
<script>
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('contact-form');
const submitBtn = document.getElementById('submit-btn');
const btnText = document.getElementById('btn-text');
const spinner = document.getElementById('spinner');
const successAlert = document.getElementById('success-message');
const errorAlert = document.getElementById('error-message');
const setLoading = (loading) => {
if (loading) {
submitBtn.disabled = true;
spinner.classList.remove('hidden');
btnText.textContent = 'Sending...';
} else {
submitBtn.disabled = false;
spinner.classList.add('hidden');
btnText.textContent = 'Send Message';
}
};
// Prevent native submission by intercepting submit event in capture phase
form.addEventListener('submit', (event) => {
// This runs before SpamBlock's handler, preventing native submission
event.preventDefault();
}, { capture: true });
// Intercept allowed event to handle submission manually
form.addEventListener('spamblock:allowed', async (event) => {
setLoading(true);
errorAlert.classList.add('hidden');
try {
// Submit form data via fetch
const formData = new FormData(form);
const response = await fetch('/api/contact', {
method: 'POST',
body: formData
});
if (response.ok) {
successAlert.textContent = '✅ Thank you! Your message has been sent successfully.';
successAlert.classList.remove('hidden');
form.reset();
setTimeout(() => successAlert.classList.add('hidden'), 5000);
} else {
throw new Error('Submission failed');
}
} catch (error) {
errorAlert.textContent = '❌ Error sending message. Please try again.';
errorAlert.classList.remove('hidden');
} finally {
setLoading(false);
}
});
// Handle blocked submissions
form.addEventListener('spamblock:blocked', (event) => {
setLoading(false);
const response = event.detail.response;
const reasons = response?.reasons || [];
let errorMsg = '❌ Your submission was blocked. ';
if (reasons.includes('disposable_domain')) {
errorMsg += 'Please use a valid email address.';
} else if (reasons.includes('profanity_detected')) {
errorMsg += 'Please remove inappropriate language.';
} else {
errorMsg += `Spam detected (score: ${response?.score || 0}).`;
}
errorAlert.textContent = errorMsg;
errorAlert.classList.remove('hidden');
});
// Show loading state when form is submitted
form.addEventListener('submit', () => {
setLoading(true);
});
});
</script>
Key points:
- Intercept the
submitevent in the capture phase to prevent native submission - Use
FormDatato collect form data - Handle success/error states with Tailwind-styled alerts
- Form stays on the same page (no navigation)
Field-Level Validation Feedback
Show Tailwind-styled validation errors on specific fields:
form.addEventListener('spamblock:blocked', (event) => {
const response = event.detail.response;
const reasons = response?.reasons || [];
// Highlight email field if disposable domain detected
if (reasons.includes('disposable_domain')) {
const emailInput = form.querySelector('[name="email"]');
emailInput.classList.remove('border-slate-300', 'focus:border-blue-500');
emailInput.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
// Show inline error message
let errorMsg = emailInput.parentElement.querySelector('.error-message');
if (!errorMsg) {
errorMsg = document.createElement('p');
errorMsg.className = 'mt-1 text-sm text-red-600 error-message';
emailInput.parentElement.appendChild(errorMsg);
}
errorMsg.textContent = 'Please use a valid, non-disposable email address.';
}
// Highlight message field if profanity detected
if (reasons.includes('profanity_detected')) {
const messageInput = form.querySelector('[name="message"]');
messageInput.classList.remove('border-slate-300', 'focus:border-blue-500');
messageInput.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
let errorMsg = messageInput.parentElement.querySelector('.error-message');
if (!errorMsg) {
errorMsg = document.createElement('p');
errorMsg.className = 'mt-1 text-sm text-red-600 error-message';
messageInput.parentElement.appendChild(errorMsg);
}
errorMsg.textContent = 'Please remove inappropriate language from your message.';
}
});
Gradient Submit Button with Loading State
<button
type="submit"
id="submit-btn"
class="w-full bg-gradient-to-r from-blue-600 to-indigo-600 text-white py-3 px-6 rounded-lg
hover:from-blue-700 hover:to-indigo-700 focus:outline-none focus:ring-2
focus:ring-blue-500 focus:ring-offset-2 font-medium transition-all duration-200
disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2
shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 disabled:transform-none">
<svg id="spinner" class="hidden animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span id="btn-text">Send Message</span>
</button>
Complete Example
Here's a complete, production-ready Tailwind form with SpamBlock:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contact Form - SpamBlock + Tailwind</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-50 min-h-screen py-12">
<div class="max-w-2xl mx-auto px-4">
<div class="bg-white rounded-xl shadow-lg p-8">
<h1 class="text-3xl font-bold text-slate-900 mb-2">Contact Us</h1>
<p class="text-slate-600 mb-8">Send us a message and we'll get back to you soon.</p>
<form
data-block-spam
action="/api/contact"
method="post"
id="contact-form"
class="space-y-6">
<div>
<label for="name" class="block text-sm font-medium text-slate-700 mb-2">
Name
</label>
<input
type="text"
id="name"
name="name"
required
class="w-full px-4 py-2 border border-slate-300 rounded-lg shadow-sm
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
transition-colors">
</div>
<div>
<label for="email" class="block text-sm font-medium text-slate-700 mb-2">
Email Address
</label>
<input
type="email"
id="email"
name="email"
required
placeholder="[email protected]"
class="w-full px-4 py-2 border border-slate-300 rounded-lg shadow-sm
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
transition-colors">
</div>
<div>
<label for="message" class="block text-sm font-medium text-slate-700 mb-2">
Message
</label>
<textarea
id="message"
name="message"
rows="5"
required
placeholder="Your message here..."
class="w-full px-4 py-2 border border-slate-300 rounded-lg shadow-sm
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
transition-colors resize-none"></textarea>
</div>
<button
type="submit"
id="submit-btn"
class="w-full bg-blue-600 text-white py-3 px-6 rounded-lg
hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500
focus:ring-offset-2 font-medium transition-colors disabled:opacity-50
disabled:cursor-not-allowed flex items-center justify-center gap-2">
<svg id="spinner" class="hidden animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span id="btn-text">Send Message</span>
</button>
<div id="success-message" class="hidden rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
<div class="flex items-center gap-2">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
<span id="success-text"></span>
</div>
</div>
<div id="error-message" class="hidden rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700">
<div class="flex items-center gap-2">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
<span id="error-text"></span>
</div>
</div>
</form>
</div>
</div>
<script src="https://api.spamblock.io/sdk/pixel/v1.js defer"></script>
<script>
// Event handling code from above
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('contact-form');
const submitBtn = document.getElementById('submit-btn');
const btnText = document.getElementById('btn-text');
const spinner = document.getElementById('spinner');
const successAlert = document.getElementById('success-message');
const successText = document.getElementById('success-text');
const errorAlert = document.getElementById('error-message');
const errorText = document.getElementById('error-text');
const hideAlerts = () => {
successAlert.classList.add('hidden');
errorAlert.classList.add('hidden');
};
const setLoading = (loading) => {
if (loading) {
submitBtn.disabled = true;
spinner.classList.remove('hidden');
btnText.textContent = 'Sending...';
} else {
submitBtn.disabled = false;
spinner.classList.add('hidden');
btnText.textContent = 'Send Message';
}
};
form.addEventListener('spamblock:allowed', (event) => {
setLoading(false);
hideAlerts();
const response = event.detail.response;
if (response && response.allow === true) {
successText.textContent = 'Thank you! Your message has been sent successfully.';
successAlert.classList.remove('hidden');
form.reset();
setTimeout(() => successAlert.classList.add('hidden'), 5000);
}
});
form.addEventListener('spamblock:blocked', (event) => {
setLoading(false);
hideAlerts();
const response = event.detail.response;
const reasons = response?.reasons || [];
let errorMsg = 'Your submission was blocked. ';
if (reasons.includes('disposable_domain')) {
errorMsg += 'Please use a valid email address.';
} else if (reasons.includes('profanity_detected')) {
errorMsg += 'Please remove inappropriate language.';
} else {
errorMsg += `Spam detected (score: ${response?.score || 0}).`;
}
errorText.textContent = errorMsg;
errorAlert.classList.remove('hidden');
});
form.addEventListener('submit', () => {
hideAlerts();
setLoading(true);
});
});
</script>
</body>
</html>
Best Practices
- Use Tailwind's state variants: Leverage
hover:,focus:,disabled:, andgroup-*for interactive states - Consistent spacing: Use Tailwind's spacing scale (
space-y-6,gap-2, etc.) for consistent layouts - Smooth transitions: Add
transition-colorsortransition-allfor smooth state changes - Accessible colors: Ensure sufficient contrast ratios for text on colored backgrounds
- Responsive design: Use Tailwind's responsive prefixes (
sm:,md:,lg:) for mobile-first design
Troubleshooting
| Issue | Solution |
|---|---|
| Styles not applying | Ensure Tailwind CSS is properly configured and compiled |
| Animations not working | Add custom animations to tailwind.config.js or use Tailwind's built-in animations |
| Hidden classes not working | Check that hidden class isn't being overridden by other styles |
| Loading spinner not visible | Ensure SVG has proper dimensions and animate-spin class is applied |