Step 1 – Install the Laravel CAPTCHA package
Install the CAPTCHA package using Composer.
composer require mews/captcha
This package generates a simple image CAPTCHA and provides validation support.
Step 2 – Configure CAPTCHA settings
Publish the CAPTCHA configuration file.
php artisan vendor:publish --provider="Mews\Captcha\CaptchaServiceProvider"
This creates the captcha.php config file in the config folder.
<?php return [ 'disable' => env('CAPTCHA_DISABLE', false), 'characters' => [ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ], 'fontsDirectory' => dirname(__DIR__) . '/vendor/mews/captcha/assets/fonts', 'bgsDirectory' => dirname(__DIR__) . '/vendor/mews/captcha/assets/backgrounds', 'default' => [ 'length' => 6, 'width' => 345, 'height' => 65, 'quality' => 90, 'math' => false, 'expire' => 60, 'encrypt' => false, ], 'flat' => [ 'length' => 6, 'fontColors' => ['#2c3e50', '#c0392b', '#16a085', '#c0392b', '#8e44ad', '#303f9f', '#f57c00', '#795548'], 'width' => 345, 'height' => 65, 'math' => false, 'quality' => 100, 'lines' => 6, 'bgImage' => true, 'bgColor' => '#28faef', 'contrast' => 0, ], 'mini' => [ 'length' => 3, 'width' => 60, 'height' => 32, ], 'inverse' => [ 'length' => 5, 'width' => 120, 'height' => 36, 'quality' => 90, 'sensitive' => true, 'angle' => 12, 'sharpen' => 10, 'blur' => 2, 'invert' => false, 'contrast' => -5, ], 'math' => [ 'length' => 9, 'width' => 120, 'height' => 36, 'quality' => 90, 'math' => true, ], ];You can use the default settings without any changes.
If needed, you can customize values like:
- length of CAPTCHA
- image width and height
- background and font colors
For example, to change the character length:
'length' => 5,
Most projects can use the default configuration without any changes.
Using different CAPTCHA styles
You can use different CAPTCHA styles defined in the configuration file, such as mini, inverse and more.
To use a specific style, pass its name to the captcha_img() function:
captcha_img('mini')captcha_img('inverse')
Make sure to update the style in all places where captcha_img() is used.
In this example, it is used in:
- the form display
- the reload CAPTCHA method
Both should use the same style to avoid mismatch.
CAPTCHA style comparison
| Style | Length | Size (approx) | Special feature | Use case |
|---|---|---|---|---|
| default | 6 | Large | Standard image | General forms |
| flat | 6 | Large | Colored text and background | Modern UI forms |
| mini | 3 | Small | Compact size | Small layouts |
| inverse | 5 | Medium | Distortion and contrast | Better readability |
| math | 9 | Medium | Math-based CAPTCHA | Stronger bot protection |
Step 3 – Create routes
use App\Http\Controllers\CaptchaController;
Route::get('/', [CaptchaController::class, 'showForm'])->name('captcha.form');
Route::post('/submit', [CaptchaController::class, 'submit'])->name('captcha.submit');
Route::get('/reload-captcha', [CaptchaController::class, 'reloadCaptcha'])->name('captcha.reload');
These routes display the form, handle form submission, and reload the CAPTCHA image.
Step 4 – Create the controller
Create a controller to display the form, validate input, and generate the CAPTCHA image.
php artisan make:controller CaptchaController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class CaptchaController extends Controller
{
public function showForm()
{
return view('captcha-form', [
'captchaImage' => captcha_img(),
]);
}
public function submit(Request $request)
{
$request->validate([
'name' => ['nullable', 'string', 'max:100'],
'email' => ['nullable', 'email'],
'captcha' => ['required', 'captcha'],
], [
'captcha.required' => 'Enter the CAPTCHA',
'captcha.captcha' => 'CAPTCHA is incorrect',
]);
return redirect()
->route('captcha.form')
->with('success', 'Form submitted successfully');
}
public function reloadCaptcha()
{
return response()->json([
'captcha' => captcha_img(),
]);
}
}
The controller displays the form, validates the CAPTCHA input, and handles the reload request.
The CAPTCHA image is generated by the mews/captcha package and displayed in the form.
On form submit, the entered value is validated using Laravel’s captcha validation rule. If it does not match, a validation error is shown.
The reload action returns a new CAPTCHA image without refreshing the page.
You can also switch CAPTCHA styles using the configuration file.
You can also switch CAPTCHA styles using the configuration file by passing the style name to the captcha_img() function.
Step 5 – Build the Blade form
Create a Blade view to display the form, CAPTCHA image, and validation messages.
resources/views/captcha-form.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Laravel CAPTCHA Form</title>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
background: #f4f6fb;
color: #1f2937;
}
.page {
max-width: 520px;
margin: 60px auto;
padding: 24px;
}
.card {
background: #ffffff;
border: 1px solid #dbe3f0;
border-radius: 12px;
padding: 24px;
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
}
h1 {
margin: 0 0 8px;
font-size: 28px;
}
p {
margin: 0 0 20px;
color: #4b5563;
}
label {
display: block;
margin-bottom: 6px;
font-weight: 700;
}
input {
width: 100%;
box-sizing: border-box;
padding: 12px;
border: 1px solid #cbd5e1;
border-radius: 8px;
margin-bottom: 16px;
font-size: 15px;
}
.captcha-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.captcha-image {
min-height: 46px;
padding: 6px;
border: 1px solid #cbd5e1;
border-radius: 8px;
background: #fff;
}
button {
border: 0;
border-radius: 8px;
padding: 12px 16px;
font-size: 15px;
cursor: pointer;
}
.reload-button {
background: #e2e8f0;
color: #1e293b;
}
.submit-button {
width: 100%;
background: #2563eb;
color: #fff;
font-weight: 700;
}
.message {
padding: 12px 14px;
border-radius: 8px;
margin-bottom: 16px;
}
.message.success {
background: #dcfce7;
color: #166534;
}
.message.error {
background: #fee2e2;
color: #b91c1c;
}
.field-error {
margin-top: -10px;
margin-bottom: 16px;
color: #b91c1c;
font-size: 14px;
}
</style>
</head>
<body>
<div class="page">
<div class="card">
<h1>Simple CAPTCHA Form</h1>
<p>Enter the CAPTCHA code shown in the image before submitting the form.</p>
@if (session('success'))
<div class="message success">{{ session('success') }}</div>
@endif
@if ($errors->has('captcha'))
<div class="message error">{{ $errors->first('captcha') }}</div>
@endif
<form method="POST" action="{{ route('captcha.submit') }}">
@csrf
<label for="name">Name</label>
<input
id="name"
type="text"
name="name"
value="{{ old('name') }}"
placeholder="Optional"
>
<label for="email">Email</label>
<input
id="email"
type="text"
name="email"
value="{{ old('email') }}"
placeholder="Optional"
>
@error('email')
<div class="field-error">{{ $message }}</div>
@enderror
<label for="captcha">CAPTCHA</label>
<div class="captcha-row">
<div class="captcha-image" id="captcha-image">{!! $captchaImage !!}</div>
<button class="reload-button" type="button" id="reload-captcha">Reload</button>
</div>
<input
id="captcha"
type="text"
name="captcha"
placeholder="Enter the CAPTCHA"
autocomplete="off"
>
<button class="submit-button" type="submit">Submit</button>
</form>
</div>
</div>
<script>
document.getElementById('reload-captcha').addEventListener('click', async function () {
const response = await fetch('{{ route('captcha.reload') }}', {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
const data = await response.json();
document.getElementById('captcha-image').innerHTML = data.captcha;
document.getElementById('captcha').value = '';
});
</script>
</body>
</html>
The form displays the CAPTCHA image and includes a reload option.
When the reload link is clicked, a new CAPTCHA image is loaded using a simple fetch request.
Validation errors and success messages are shown in the form after submit.
