A bunch of reactive utility types and functions, for building primitives
| Stage | Category | Version | Last Updated |
|---|---|---|---|
| 3 | Utilities | 7.0.0-next.4 (next) | Aug 13, 2026 |
npm i @solid-primitives/utils@nextSolid Primitives Utilities is a support and helper package for a number of primitives in our library. Please free to augment or centralize useful utilities and methods in this package for sharing.
Immutable helpers
Functional programming helpers for making non-mutating changes to data. Keeping it immutable. Useful for updating signals.
import { pick } from "@solid-primitives/utils/immutable";
const original = { foo: 123, bar: "baz" };const newObj = pick(original, "foo");original; // { foo: 123, bar: "baz" }newObj; // { foo: 123 }Use it for changing signals:
import { push, update } from "@solid-primitives/utils/immutable";
const [list, setList] = createSignal([1, 2, 3]);setList(p => push(p, 4));
const [user, setUser] = createSignal({ name: "John", street: { name: "Kingston Cei", number: 24 },});setUser(p => update(p, "street", "number", 64));List of functions:
Copying
shallowArrayCopy- make shallow copy of an arrayshallowObjectCopy- make shallow copy of an objectshallowCopy- make shallow copy of an array/objectwithArrayCopy- apply mutations to the an array without changing the originalwithObjectCopy- apply mutations to the an object without changing the originalwithCopy- apply mutations to the an object/array without changing the original
Array
push- non-mutatingArray.prototype.push()drop- non-mutating function that drops n items from the array startdropRight- non-mutating function that drops n items from the array endfilterOut- standaloneArray.prototype.filter()that filters out passed itemfilter- standaloneArray.prototype.filter()sort- non-mutatingArray.prototype.sort()as a standalone functionsortBy- Sort an array by object key, or multiple keysmap- standaloneArray.prototype.map()functionslice- standaloneArray.prototype.slice()functionsplice- non-mutatingArray.prototype.splice()as a standalone functionfill- non-mutatingArray.prototype.fill()as a standalone functionconcat- Creates a new array concatenating array with any additional arrays and/or values.remove- Remove item from arrayremoveItems- Remove multiple items from an arrayflatten- Flattens a nested array into a one-level arrayfilterInstance- Flattens a nested array into a one-level arrayfilterOutInstance- Flattens a nested array into a one-level array
Object
omit- Create a new subset object without the provided keyspick- Create a new subset object with only the provided keyssplit- Split object into multiple subset objects.merge- Merges multiple objects into a single one.
Object/Array
get- Get a single property value of an object by specifying a path to it.update- Change single value in an object by key, or series of recursing keys.
Number
add-a + b + c + ...(works for numbers or strings)substract-a - b - c - ...multiply-a * b * c * ...divide-a / b / c / ...power-a ** b ** c ** ...clamp- clamp a number value between two other values
String transforms
(string) => T transform functions for converting raw string data into typed values. Useful as the transform option for SSE, WebSocket, and similar streaming primitives.
import { json, ndjson, safe } from "@solid-primitives/utils";
const { data } = createSSE<Event>(url, { transform: json });const { data } = createSSE<Event[]>(url, { transform: ndjson });const { data } = createSSE<Event>(url, { transform: safe(json) });json- Parse a string as a single JSON valuendjson- Parse newline-delimited JSON (NDJSON / JSON Lines) into an arraylines- Split a string into astring[]by newline, filtering empty linesnumber- Parse a string as a number viaNumber()safe(transform, fallback?)- Wrap any transform in atry/catch; returnsfallbackinstead of throwingpipe(a, b)- Compose two transforms into one
wrapSetter
It is a typical use case to react on setting a new value; this is especially cumbersome for stores, where you otherwise need the deep package to make effects subscribe to all changes. A more performant and simple approach is to wrap the setter of your signal or store. To simplify this approach, we provide a wrapSetter function:
import { createStore } from "solid-js";import { wrapSetter } from "@solid-primitives/utils";
const [state, setState] = wrapSetter( createStore( localStorage.getItem("persistedState") ? JSON.parse(localStorage.getItem("persistedState")) : initialState, ), setter => next => { const output = setState(next); localStorage.setItem( "persistedState", latest(() => JSON.stringify(state)), ); return output; },);If the signal or store is destructured into a tuple and augmented with additional values, those are left intact in the output. For the TS types to work, you need to as const the new tuple:
import { createSignal } from "solid-js";import { wrapSetter } from "@solid-primitives/utils";
const augmentedSignal = [...createSignal(0), { extra: "data" }] as const;const [count, setCount, data] = wrapSetter( augmented, setter => next => (console.log(next), setter(next)),);Color utilities
Multi-format color parsing, conversion, and accessibility naming — available as a separate subpath so you only pay for what you use.
Credits — The core color types, parsing, and color-space conversion logic (
types.ts,helpers.ts,intl.ts) are adapted from Kobalte (MIT, Copyright Fabien Marie-Louise), which itself credits the React Spectrum team (Apache 2.0, Copyright 2020 Adobe). The OKLCH inverse pipeline inmanipulation.tsuses coefficients from Björn Ottosson's OKLab work.
import { parseColor, normalizeColor, getColorChannels, normalizeHue,} from "@solid-primitives/utils/colors";import { COLOR_INTL_TRANSLATIONS } from "@solid-primitives/utils/colors";Parsing & conversion
const color = parseColor("#3264C8");
color.toString(); // "rgb(50, 100, 200)" (css default)color.toString("hsl"); // "hsl(220 60% 49.02%)"color.toString("hsb"); // "hsb(220 75% 78.43%)"color.toString("hex"); // "#3264C8"color.toString("hexa"); // "#3264C8FF"
// Convert to another space and inspect channelsconst hsl = color.toFormat("hsl");hsl.getChannelValue("hue"); // 220hsl.getChannelValue("saturation"); // 60hsl.getChannelValue("lightness"); // 49.02
// Derive a new color by changing one channelconst lighter = hsl.withChannelValue("lightness", 70);lighter.toString("hex"); // "#6699E8"Accepted input formats:
| Format | Examples |
|---|---|
| Hex | #rgb, #rgba, #rrggbb, #rrggbbaa |
| RGB | rgb(50 100 200), rgb(50, 100, 200), rgb(50 100 200 / 0.5), rgba(50, 100, 200, 0.5) |
| HSL | hsl(220, 60%, 49%), hsla(220, 60%, 49%, 0.5) |
| HSB | hsb(220, 75%, 78%), hsba(220, 75%, 78%, 0.5) |
parseColor throws for unrecognised strings. Use normalizeColor when your input may already be a Color object:
function accept(value: string | Color) { const color = normalizeColor(value); // no-op if already a Color}Channel metadata
// Ordered channel names for a spacegetColorChannels("rgb"); // ["red", "green", "blue"]getColorChannels("hsl"); // ["hue", "saturation", "lightness"]getColorChannels("hsb"); // ["hue", "saturation", "brightness"]
// Range info for driving sliderscolor.getChannelRange("red"); // { minValue: 0, maxValue: 255, step: 1, pageSize: 17 }color.getChannelRange("alpha"); // { minValue: 0, maxValue: 1, step: 0.01, pageSize: 0.1 }
// Intl format options for displaycolor.getChannelFormatOptions("hue"); // { style: "unit", unit: "degree", unitDisplay: "narrow" }
// Locale-aware formatted valuecolor.toFormat("hsl").formatChannelValue("saturation"); // "60%"
// Axis layout for a 2D color area pickercolor.getColorSpaceAxes({ xChannel: "saturation", yChannel: "brightness" });// { xChannel: "saturation", yChannel: "brightness", zChannel: "hue" }Hue normalization
normalizeHue(370); // 10normalizeHue(-10); // 350normalizeHue(360); // 360 — kept distinct from 0 for full-circle slidersAccessible color naming
getColorName converts to OKLCH — a perceptually uniform space — and produces a human-readable description suitable for aria-label and similar attributes. Pass COLOR_INTL_TRANSLATIONS for English or supply your own translations object.
import { parseColor, COLOR_INTL_TRANSLATIONS } from "@solid-primitives/utils/colors";
const t = COLOR_INTL_TRANSLATIONS;
parseColor("#3264C8").getColorName(t); // "dark vibrant blue"parseColor("hsl(48 80% 55%)").getColorName(t); // "orange"parseColor("rgb(50 100 200 / 0.2)").getColorName(t); // "dark vibrant blue, 80% transparent"
parseColor("#3264C8").getHueName(t); // "blue"parseColor("#c86432").getHueName(t); // "brown"
// Channel label for a slider aria-labelparseColor("#fff").getChannelName("saturation", t); // "Saturation"Providing translations for other locales:
import type { ColorIntlTranslations } from "@solid-primitives/utils/colors";
const frTranslations: ColorIntlTranslations = { ...COLOR_INTL_TRANSLATIONS, hue: "Teinte", saturation: "Saturation", lightness: "Luminosité", blue: "bleu", // ...};Safe parsing
tryParseColor("#3264C8"); // ColortryParseColor("nope"); // undefined — never throws
isValidColor("#3264C8"); // trueisValidColor("nope"); // false
detectColorFormat("rgb(50, 100, 200)"); // "rgb"detectColorFormat("#3264C8FF"); // "hexa"detectColorFormat("hsla(220, 60%, 49%, 0.5)"); // "hsla"detectColorFormat("nope"); // undefinedManipulation
All manipulation functions return a new Color; the input is never mutated. amount is a 0–1 ratio representing percentage points on the relevant HSL channel (e.g. 0.1 = 10 pp).
import { lighten, darken, saturate, desaturate, complement, mix,} from "@solid-primitives/utils/colors";
lighten(color, 0.1); // +10 lightness (HSL)darken(color, 0.1); // −10 lightness (HSL)saturate(color, 0.2); // +20 saturation (HSL)desaturate(color, 0.2); // −20 saturation (HSL)complement(color); // hue + 180° (HSL)
mix(black, white); // 50/50 blend in RGBmix(black, white, 0.25); // 25% toward whiteAccessibility (WCAG 2.1)
import { contrastRatio, isReadable } from "@solid-primitives/utils/colors";
contrastRatio(parseColor("#000"), parseColor("#fff")); // 21contrastRatio(parseColor("#3264C8"), parseColor("#fff")); // ~3.9
// AA / AAA, normal / large textisReadable(fg, bg); // AA normal (≥ 4.5 : 1)isReadable(fg, bg, "AA", "large"); // AA large (≥ 3.0 : 1)isReadable(fg, bg, "AAA"); // AAA normal (≥ 7.0 : 1)isReadable(fg, bg, "AAA", "large"); // AAA large (≥ 4.5 : 1)Color scales
import { colorScale, perceptualColorScale } from "@solid-primitives/utils/colors";
// 5-step RGB gradient from black to whitecolorScale(parseColor("#000"), parseColor("#fff"), 5);// [#000000, #404040, #808080, #BFBFBF, #FFFFFF]
// 5-step perceptual gradient (OKLCH space, shortest hue arc)// Red → Blue goes through purple, not greenperceptualColorScale(parseColor("hsl(0, 80%, 50%)"), parseColor("hsl(240, 80%, 50%)"), 5);List of exports
Parsing
parseColor(value)- Parse a color string; throws on invalid inputnormalizeColor(value)- Accept a string orColor, always return aColortryParseColor(value)- LikeparseColorbut returnsundefinedinstead of throwingisValidColor(value)- Boolean check without parsing overheaddetectColorFormat(value)- Detect the syntax format of a color string
Color space / channels
getColorChannels(space)- Ordered channel triple for a color spacenormalizeHue(hue)- Wrap a hue angle to[0, 360]colorToOKLCH(color)- Convert anyColorto[L, C, H]in OKLCH space
Manipulation
lighten(color, amount)/darken(color, amount)- Adjust HSL lightnesssaturate(color, amount)/desaturate(color, amount)- Adjust HSL saturationcomplement(color)- Hue + 180°mix(a, b, ratio?)- Blend two colors in RGB space
Accessibility
contrastRatio(a, b)- WCAG 2.1 contrast ratio (1–21)isReadable(fg, bg, level?, size?)- WCAG 2.1 AA / AAA compliance check
Color scales
colorScale(from, to, steps)- Linear RGB gradientperceptualColorScale(from, to, steps)- Perceptually uniform OKLCH gradient
i18n / naming
COLOR_INTL_TRANSLATIONS- Default English translationsColorIntlTranslations(type) - Shape of a translations object
Types
Color(interface) - The immutable color value typeColorFormat(type) -"hex" | "hexa" | "rgb" | "rgba" | "hsl" | "hsla" | "hsb" | "hsba"ColorSpace(type) -"rgb" | "hsl" | "hsb"ColorChannel(type) -"hue" | "saturation" | "brightness" | "lightness" | "red" | "green" | "blue" | "alpha"ColorAxes(type) -{ xChannel, yChannel, zChannel: ColorChannel }ColorChannelRange(type) -{ minValue, maxValue, step, pageSize }
Changelog
See CHANGELOG.md