abeautifulsite/jquery-minicolors
jQuery MiniColors is a tiny, dependency-light color picker plugin for jQuery. It offers an input and inline mode, optional alpha channel, swatches, themes, and keyboard support, and works across modern browsers with minimal setup.
abeautifulsite/jquery-minicolors (published on npm as @claviska/jquery-minicolors) is a small jQuery color-picker plugin. In a Laravel app it's used almost exclusively to attach a picker to a text input that stores a hex color string — themes, brand colors, tag colors, chart series, avatar backgrounds.
1. Install (npm preferred over CDN):
npm install @claviska/jquery-minicolors
2. Wire it up in your bundler. It needs jQuery, so make sure jQuery is global before the plugin loads:
// resources/js/bootstrap.js
import $ from 'jquery';
window.$ = window.jQuery = $;
import '@claviska/jquery-minicolors';
3. Import the CSS. The plugin ships both a default theme and a Bootstrap theme under node_modules/@claviska/jquery-minicolors/:
/* resources/css/app.css */
@import '@claviska/jquery-minicolors/jquery.minicolors.css';
/* or, for Bootstrap 4/5 look: */
/* @import '@claviska/jquery-minicolors/jquery.minicolors.bootstrap.css'; */
4. First use case — a settings form field:
{{-- resources/views/settings/edit.blade.php --}}
<input type="text"
name="brand_color"
id="brand_color"
class="minicolors form-control"
value="{{ old('brand_color', $settings->brand_color ?? '#1a73e8') }}">
// resources/js/app.js
$(() => {
$('.minicolors').minicolors({ format: 'hex', theme: 'bootstrap' });
});
Where to look first: the input receives the picker; the value is still just a hex string, so everything downstream is normal Laravel validation and Eloquent. Don't look for a PHP API — there isn't one. It's purely a frontend concern.
Validate on the server, always. The picker is a UI convenience; a user can type anything. Use Laravel's regex rule for hex:
$request->validate([
'brand_color' => ['required', 'string', 'regex:/^#[0-9A-Fa-f]{6}$/'],
]);
If you support alpha (data-opacity), validate #RRGGBBAA instead, or normalise before saving:
$data['brand_color'] = strtolower($request->input('brand_color'));
A Blade component wrapper keeps markup consistent across forms and preserves old() values on validation failure:
{{-- resources/views/components/color-input.blade.php --}}
@props(['name', 'value' => '#000000', 'format' => 'hex'])
<input type="text"
name="{{ $name }}"
{{ $attributes->merge(['class' => 'minicolors form-control']) }}
value="{{ old($name, $value) }}"
data-format="{{ $format }}">
Then <x-color-input name="brand_color" :value="$settings->brand_color" />. Because options like data-format, data-opacity, and data-theme are read per-input, you can vary picker behaviour without touching JS.
Configure via data attributes for per-field options. This is the idiomatic pattern when several pickers need different behaviour:
<input class="minicolors" data-format="rgb" data-opacity="0.5" name="overlay" value="rgba(0,0,0,0.5)">
<input class="minicolors" data-swatches='[["#000","#333","#666"],["#fff","#eee","#ddd"]]' name="accent" value="#333">
Options passed to .minicolors({...}) in JS take precedence over some data attributes, so pick one source of truth — I'd use data attributes inside components and JS defaults for the rest.
Livewire. The picker mutates the DOM behind Livewire's back, so you must use wire:ignore and re-init after component updates:
<div wire:ignore>
<input type="text" class="minicolors" wire:model="brand_color" ...>
</div>
document.addEventListener('livewire:init', () => {
Livewire.hook('morphed', ({ el, component }) => {
$(el).find('.minicolors').minicolors({ format: 'hex' });
});
});
Use wire:model.blur or debounce, since the plugin fires a change event on the input rather than input events while dragging. If you need live previews, listen for change and set the model manually:
$(document).on('change', '.minicolors', function () {
this.dispatchEvent(new Event('input', { bubbles: true }));
});
Alpine.js. A tiny directive wrapper is cleaner than scattering jQuery:
Alpine.directive('colorpicker', (el, { expression }) => {
$(el).minicolors({
format: 'hex',
change: function (value) { if (expression) Alpine.evaluate(el, expression); }
});
});
Dynamic rows (repeaters / nested forms). The plugin only attaches to elements present at init time. Re-run .minicolors() on newly inserted inputs, then clean up when rows are removed:
$('#add-row').on('click', () => {
const row = makeRow();
$('#rows').append(row);
row.find('.minicolors').minicolors({ format: 'hex' });
});
Teardown. The plugin provides destroy — call it before removing a row or you'll leak DOM wrappers:
row.find('.minicolors').minicolors('destroy');
row.remove();
$ is not a function / $(...).minicolors is not a function almost always means the plugin ran before jQuery was assigned to window, or before the DOM was ready. Assign window.$ in bootstrap.js, import the plugin after that, and initialise inside $(() => {}) or DOMContentLoaded.window.$ = window.jQuery = $ executes before any code that calls $(...).minicolors(), and avoid importing the plugin twice (once via npm, once via a <script> tag) — you'll end up with two jQuery instances and the plugin attached to the wrong one.theme: 'bootstrap' (and import jquery.minicolors.bootstrap.css) or add data-theme="bootstrap". Without the matching stylesheet the picker renders unstyled and can look broken. In Bootstrap 5/Tailwind projects, the default theme is often a better starting point than the legacy bootstrap theme.change, not input. Frameworks and validators listening only for input events won't see picker updates. If you're using Livewire, Alpine, or any reactive code, bridge change → input explicitly (see above).data-opacity changes the stored format. With opacity enabled, the plugin produces rgba() or 8-digit hex depending on format. Decide your format up front and normalise before saving, or your regex rule will start failing intermittently.wire:ignore, the picker's wrapper DOM gets replaced and the input reverts to a plain text field. This is the single most common Laravel-specific bug.$('#myInput').data('minicolors') (if set) or inspecting $('.minicolors').length in the console tells you whether init ran. If the wrapper <div class="minicolors-inline"> doesn't exist in the DOM, the plugin never attached..minicolors() calls throughout your JS — swapping it later (for a vanilla input[type=color] or a Web Component) becomes a one-file change.How can I help you explore Laravel packages today?