mexitek/phpcolors
phpColors is a lightweight PHP library for color manipulation: convert between HEX, RGB, and HSL, lighten or darken shades, generate complementary colors, and check contrast. Useful for building palettes, themes, and dynamic CSS without external dependencies.
Install with Composer — there is no service provider, config file, or facade to register:
composer require mexitek/phpcolors
Everything lives in one class. Instantiate it with a hex string (with or without #) and call the transformation you need:
use Mexitek\PHPColors\Color;
$brand = new Color('336699');
$base = $brand->getHex(); // "336699"
$hover = $brand->getDarken(15); // darker hex for hover state
$active = $brand->getDarken(25); // darker still for :active
$text = $brand->getForeground(); // black or white, whichever contrasts
That hover/contrast pair is the first use case worth wiring up: it is the reason most teams pull this package in. A button component needs a darker shade on hover and a readable label color, and both are one call each.
Where to look first:
src/Color.php — the entire package. Read it once; it is small enough to finish in a few minutes, and it tells you exactly which methods return a # and which return a plain 6-character string.getDarken, getLighten, getComplementary, getTriad, getTetrad, getAnalogous, getSplitComplementary, getMix, isDark, getForeground, plus static equivalents).Generate a full shade ramp from one brand color. This is the highest-value pattern: turn a single client-supplied color into a design-token scale.
$base = new Color($brandHex);
$scale = [];
foreach ([90, 70, 50, 30, 10] as $i => $step) {
$scale["brand-{$i}00"] = '#' . $base->getLighten($step);
}
Compute every step from the same base rather than chaining getLighten() calls; chaining compounds the shift (see Gotchas).
Pick readable text on arbitrary backgrounds.
$bg = new Color($userColor);
$fg = $bg->isDark() ? '#ffffff' : '#000000';
// or simply:
$fg = $bg->getForeground();
Useful for badges, avatar initials, tag chips, and calendar event blocks where the background is data-driven.
Emit CSS custom properties from a theme record. In a controller, service, or Blade partial:
$theme = [
'primary' => $c->getHex(),
'primary-hover' => $c->getDarken(12),
'primary-subtle' => $c->getMix('ffffff', 85),
'primary-text' => $c->getForeground(),
];
Then render --color-primary: #{{ $theme['primary'] }}; in a <style> block. This keeps color math out of your CSS build and lets admins reskin an app without a deploy.
Harmonies for categorical data. getComplementary(), getTriad(), getTetrad(), getAnalogous(), and getSplitComplementary() return arrays of hex strings — good for assigning distinct, related series colors in charts or labels in a tag cloud. Cache the result per base color; the array shapes are stable.
Use the static API for one-shot transforms. When you do not need a persistent object, Color::darken($hex, 20), Color::lighten($hex, 20), Color::mix($hex1, $hex2, 50), and Color::isDark($hex) keep call sites short. Reach for the instance API when you are deriving several values from one color.
Normalize at your boundary, not in your views. Wrap the library in a tiny helper or value object that guarantees the leading #, validates input, and falls back to a default brand color. Every Blade template and API response then receives consistently formatted strings, and swapping the library later means editing one file.
Validate user-supplied colors before construction.
$hex = ltrim($request->input('color', '336699'), '#');
if (! Color::isValidHex($hex)) {
$hex = config('brand.default');
}
getDarken(10) subtracts roughly ten points of L. Looping getDarken(5) five times does not equal getDarken(25) and can drive L to zero. Always derive from the original base and clamp your inputs to 0–100.#. Read src/Color.php once and standardize with a single '#' . ltrim($hex, '#') helper rather than guessing per call site.336698 where you started with 336699. Do not assert exact string equality in tests against computed shades; assert channel deltas within a tolerance.rgba(), CSS named colors, and alpha channels are not part of the model. Normalize and expand before constructing.isDark() is a brightness heuristic, not a WCAG contrast calculation. It answers "is this roughly dark?" — getForeground() then picks pure black or pure white. Mid-tone backgrounds can land on a pairing below the 4.5:1 AA ratio. For accessibility-critical UI, compute the real contrast ratio from getRgb() and choose your own foreground, or add a small contrastRatio() helper alongside the library.Color::isValidHex() first if the value comes from a user or database; otherwise you get surprising output rather than a clear exception.Color (to add withHash(), a contrast helper, or a toCssVar() method) or wrapping it in your own service. Prefer the wrapper — it keeps the library out of your templates.How can I help you explore Laravel packages today?