awcodes/palette
A Filament Forms color picker field that uses preset color palettes. Stores selections as structured arrays or keys, supports Filament 4.x and 5.x, and offers publishable config for global customization.
Install and wire it up. Three steps, one of which everyone forgets:
composer require awcodes/palette
php artisan vendor:publish --tag="palette-config" # only if you need to change defaults
Then add the vendor views to your theme CSS (Filament panel) or app CSS (standalone):
@source '../../../../vendor/awcodes/palette/resources/**/*.blade.php';
Cast the column before you write the field. Default storage is a structured array, so the migration's column needs ->json() and the model needs:
protected $casts = ['color' => 'array'];
Skip this and the form 500s on submit — not on render. It is the single most common first-integration failure.
Where to look first. The package is small enough to read end-to-end in ten minutes and you should, because there is no downstream ecosystem pressure-testing it:
src/Forms/Components/ColorPicker.php and ColorPickerSelect.php — the public API and the argument parsing.config/palette.php — the global default for store_as_key, plus whatever defaults you publish.resources/views/ — the Blade, which is where the @source glob points and where the style hook classes live.First use case. Almost every adoption starts as a "brand/category color" field on a resource:
use Awcodes\Palette\Forms\Components\ColorPicker;
use Filament\Support\Colors\Color;
ColorPicker::make('color')
->colors([
'indigo' => Color::Indigo,
'emerald' => Color::Emerald,
'salmon' => '#fa8072',
])
->size('sm');
That is a working, persisted field. Everything else in the package is refinement on top of this.
Decide array vs storeAsKey() on day one. This is a one-line form-definition decision with migration-shaped consequences. Default to the rich array: it stores key, property, label, type, value, so you can re-label, re-shade, or correct a hex later without touching the database.
Keep palette definitions in your own code, not in form definitions. The colors array is the actual design artifact — curate it in one place so every resource agrees:
// app/Support/Palette.php
final class Palette
{
public static function brand(): array
{
return [
'primary' => Color::Indigo,
'secondary' => Color::Emerald,
'accent' => Color::Amber,
'danger' => Color::Rose,
'neutral' => Color::Slate,
];
}
public static function labels(): array
{
return ['accent' => 'Highlight Accent'];
}
}
ColorPicker::make('color')
->colors(Palette::brand())
->labels(Palette::labels())
->withBlack(swap: '#111111')
->withWhite(swap: '#f5f5f5');
This is also your cheapest insurance against the maintainer bus factor: if you ever fork, your palette definitions are already yours.
Constrain with shades instead of offering a full ramp. shades() pins each color to one step, which is exactly what you want for "pick a category color" — the user chooses which color, not which shade:
->colors(['indigo' => Color::Indigo, 'emerald' => Color::Emerald])
->shades(['indigo' => 500, 'emerald' => 600])
Shades only apply to Filament Color:: constants. Passing '#fa8072' or a class string and expecting a shade is a silent no-op.
Mix ColorPicker and ColorPickerSelect by context. Both accept the same colors/shades/labels/withBlack/withWhite chain, so swapping is a one-line change. ColorPicker (swatch grid) for visual design surfaces where the user is choosing a look; ColorPickerSelect for admin/CRUD forms where the palette is long and a dropdown scans faster. Define the palette once, feed both.
Class-string options are for gradients and design-token presets, and they need a label. The 'bg-gradient-secondary' => 'bg-gradient-secondary' form stores the class name as the value rather than a color. Always pair it with ->labels([...]), or users see a raw Tailwind class in the UI.
Render the stored array outside Filament with a small helper. The structured payload is the reason to pick the array mode, so use it — emit CSS custom properties rather than a fixed hex:
public static function css(array $color): string
{
return match ($color['type']) {
'rgb' => "{$color['property']}: rgb({$color['value']})",
default => "{$color['property']}: {$color['value']}",
};
}
<div style="{{ \App\Support\Palette::css($record->color) }}">…</div>
That gives you theming (swap --primary-500 in a stylesheet) without a data migration. If you chose storeAsKey(), resolve the key back through Palette::brand() before rendering — the key alone carries no value.
Use ColorEntry in infolists rather than hand-rolling a swatch. It resolves both array and key storage (fixed in v3.2.0) and takes the same size() scale, so view and edit screens stay visually consistent.
Style with the hook classes, not by overriding vendor Blade. palette-color-picker, palette-color-picker-item, palette-color-picker-item-active on the picker and palette-entry-item on the entry. Small, stable selectors — prefer them over reaching into the published-view hierarchy.
Test the round trip, not the component. A Livewire form test that fills the field and asserts the persisted shape catches the missing cast, the storeAsKey mismatch, and the config default all at once:
livewire(CreateCategory::class)
->fillForm(['color' => 'primary'])
->call('create')
->assertHasNoFormErrors();
$this->assertSame('primary', Category::first()->color['key']);
Dynamically-passed Tailwind classes won't be compiled. The @source glob scans Palette's Blade, but a class supplied from your PHP ('bg-gradient-secondary') never appears literally in any scanned file, so Tailwind won't emit it. Either define those utilities in a file you already scan (or a safelist), or the swatch renders transparent with no error anywhere.
The @source path is relative to the CSS file, and it fails silently. If your theme lives at a different depth than the README assumes, the glob matches nothing and you get an unstyled-but-functional picker. It's an asset-pipeline dependency, not a Composer one — a theme scaffold change during a Filament upgrade can strip the styles with zero Composer signal. Verify visually after any theme work, and treat "picker looks like a plain select" as a build problem, not a package bug.
Outside a Filament panel, add @stack('styles'). Since v1.1.0 the select picker pushes styles to that stack; without it you get the broken-styling reports the release notes describe.
storeAsKey() is sticky and inverted-easy to fix the wrong way. v3.2.0 added honouring storeAsKey(false) on a field where the config default is true — before that, a global config flip silently overrode the per-field call. Pin >=3.2.0, and prefer setting the mode explicitly on every field rather than letting the config file decide implicitly. Changing modes later is a data migration across every table with a color column plus every read path.
Cast errors surface at submit, not at mount. A json/array cast mismatch produces a failure mid-form in staging only if that path is exercised. Add the cast in the same PR as the field, and cover the store path with a test.
Shades and labels key off the array key, not the color. shades(['badass' => 300]) requires that 'badass' is the literal key in colors(). Reordering or renaming keys in your central palette silently drops shades and labels — keep the palette class and its labels()/shades() maps adjacent so they're edited together.
Black and white need a swap for dark/neutral-heavy palettes. withBlack(swap: '#111111') and withWhite(swap: '#f5f5f5') exist because pure #000/#fff look wrong against Filament's own surfaces. Use them when the picker feeds backgrounds rather than accents.
Version-matrix prose is unreliable — pin by constraint. The README table implies 3.x covers Filament 4 and 5, while releases label v2.0.0 "v4 support" and v3.0.0 "v5 support." Set the Composer constraint from your Filament version and lock it, rather than trusting the table.
Budget for reading the source. 25 stars, zero dependents, single author across every release. The code is thin and the licence makes a fork trivial — but a fork is only a real mitigation if someone on the team can read Filament field internals. Keep your palettes, labels, and CSS-rendering helper in your own namespace so that if you ever need to take the two component classes in-house, nothing above them changes.
Don't put it through architecture review. It's two form fields, an infolist component, and a config file. No routes, no migrations, no boot-time service provider work. Assign an owner, add the cast, verify the swatches render in a built asset bundle, and move on.
How can I help you explore Laravel packages today?