News

Extract an Image's Dominant Color in Laravel

Published
Extract an Image's Dominant Color in Laravel image

An image that has not finished downloading leaves a hole in the page. The usual fixes are a gray box, a blurred thumbnail encoded into the HTML, or nothing at all. A cheaper option that covers most of the value is to fill the space with the average color of the photo, so the layout reads as intentional before a single pixel of the real image arrives.

Laravel 13.24 added dominantColor() to the first-party image API, which gives you that color without a separate package. This article covers how to get the color, where to store it, and what to do with it once you have it.

Setup

The image component's GD and Imagick drivers are backed by Intervention Image, which is a suggested dependency rather than a required one:

composer require intervention/image:^4.0

Nothing else is needed. Dominant color detection works on both drivers.

Getting the Dominant Color

Call dominantColor() on any image instance and you get a hex string back:

use Illuminate\Support\Facades\Image;
 
$color = Image::fromPath(storage_path('app/photo.jpg'))->dominantColor();
// "#8a6f4c"

The implementation resizes a copy of the image down to a single pixel and reads that pixel's value. That is an average rather than a most-frequent color, so a photo of a red barn in a green field gives you a muddy olive, not red. For a placeholder that is exactly what you want, since the color needs to sit behind the whole image rather than represent its subject.

The hash prefix is always included. Images with an alpha channel return the eight digit form (#rrggbbaa), so size the column accordingly if you store it.

Transformations are applied before the color is sampled, which means you get the color of the image you are about to save rather than the original:

$color = Image::fromStorage('uploads/photo.jpg')
->crop(800, 600, x: 200, y: 0)
->dominantColor();

Crop away the sky and the color changes.

Storing the Dominant Color With the Upload

The color is only useful if it is available when you render the page, so compute it once at upload time and keep it on the model. Start with a column:

Schema::create('photos', function (Blueprint $table) {
$table->id();
$table->string('path');
$table->string('dominant_color', 9)->nullable();
$table->timestamps();
});

Then in the controller, build the image once and reuse the instance:

use App\Models\Photo;
 
public function store(Request $request)
{
$request->validate([
'photo' => ['required', 'image', 'max:12288'],
]);
 
$photo = $request->image('photo')
->orient()
->scale(width: 1600)
->optimize();
 
return Photo::create([
'path' => $photo->store('photos'),
'dominant_color' => $photo->dominantColor(),
]);
}

Order matters here: store() runs the pipeline, caches the processed bytes on the instance, and clears the pipeline. A later dominantColor() call on the same instance samples those cached bytes and does no extra work. Reverse the two lines and the color is computed against a throwaway clone of the pipeline, leaving store() to process the image a second time. Same result, twice the work.

The value is also memoized per instance, so calling dominantColor() again is free. Because every transformation returns a new instance, a thumbnail branched off the same source computes its own color rather than inheriting one:

$source = Image::fromStorage('uploads/photo.jpg')->orient();
 
$thumbnail = $source->cover(300, 300);
$display = $source->scale(width: 1600);
 
$thumbnail->dominantColor(); // sampled from the cropped square
$display->dominantColor(); // sampled from the full frame

Using the Dominant Color as a Placeholder

With the color on the model, the template is a background color on a box that already has the right aspect ratio:

<div class="aspect-[3/2] overflow-hidden rounded-lg"
style="background-color: {{ $photo->dominant_color }}">
<img src="{{ Storage::url($photo->path) }}"
alt="{{ $photo->caption }}"
loading="lazy"
class="h-full w-full object-cover">
</div>

The colored box is painted with the first frame, the image fades in over it when it arrives, and the page never reflows. This pairs well with loading="lazy" on a long gallery, where most images are not fetched until the reader scrolls.

Readable Text Over the Color

Captions or icons placed on top of the placeholder need to stay legible whether the photo is a snowfield or a night sky. Relative luminance gives you a reasonable threshold in a few lines:

namespace App\Support;
 
class Color
{
public static function isLight(string $hex): bool
{
[$r, $g, $b] = sscanf(substr($hex, 0, 7), '#%02x%02x%02x');
 
// Rec. 709 luma, 0-255.
return (0.2126 * $r + 0.7152 * $g + 0.0722 * $b) > 140;
}
}
<figcaption class="{{ Color::isLight($photo->dominant_color) ? 'text-gray-900' : 'text-white' }}">
{{ $photo->caption }}
</figcaption>

Letterbox and Rotation Fills

The same sampling is available as a background value inside a transformation. Passing dominant to contain() fills the letterbox bars with the image's own color instead of a fixed one you chose in advance:

Image::fromUpload($request->file('photo'))
->contain(1200, 800, background: 'dominant')
->store('photos');

A portrait photo padded to a landscape frame gets bars that belong to the photo, which reads far better in a grid than white or black bars on every third tile. rotate() takes the same value for the corners it exposes:

$image->rotate(8, background: 'dominant');

In both cases the sampling happens against the image at that point in the pipeline, so a crop earlier in the chain changes the fill.

What It Is Not

dominantColor() returns one averaged color. It is not a palette extractor, and it will not find the accent color a designer would pick out of a photo. If you need a set of representative swatches, or perceptual clustering that ignores a large flat background, this is not that feature and a dedicated library is still the right call.

For placeholders, letterbox fills, and tinting a card behind its own artwork, one averaged color is the whole job, and it now ships with the framework.

Further Reading

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Filed in

Sponsored

masteringlaravel logo
Laravel Code Review

Get expert guidance in a few days with a Laravel code review

Visit Laravel Code Review

The latest

View all →
Laravel Boost Project Rules: Teach Agents Your Conventions image

Laravel Boost Project Rules: Teach Agents Your Conventions

Read article
Image Dominant Color and HEIC Support in Laravel 13.24 image

Image Dominant Color and HEIC Support in Laravel 13.24

Read article
Official Laravel Zed Extension: LSP for PHP & Blade image

Official Laravel Zed Extension: LSP for PHP & Blade

Read article
Laravel Head: Manage Meta Tags, Open Graph, and JSON-LD image

Laravel Head: Manage Meta Tags, Open Graph, and JSON-LD

Read article
PhpStorm 2026.2 Released image

PhpStorm 2026.2 Released

Read article
Laravel Doctor: Diagnose Your App With One Artisan Command image

Laravel Doctor: Diagnose Your App With One Artisan Command

Read article