iridis

iridis

Give it a few colors or a photo — get back a full, accessible color palette for your app.

A chromatic engine that resolves any seeds — or any image — into a full, contrast-enforced, OKLCH-native palette.

iridis takes a handful of seed colors — or a photo — and turns them into a complete, accessible color palette for your app: every button, every block of text, every background, all guaranteed to read clearly together.

Try a theme
Light
Dark
Every color here is engine.run().
1. What is Iridis

iridis is a chromatic pipeline for dynamic palette derivation. You give it seed colors (hex, RGB, OKLCH, etc.). It runs them through a registered sequence of tasks—intake, role resolution, contrast enforcement, variant derivation, and emission—and returns a role-resolved palette plus any consumer-shaped outputs you requested (CSS variables, Tailwind, Shadcn, MUI, Capacitor, etc.).

The core (@studnicky/iridis) ships with zero runtime dependencies. Each output target is a separate plugin package.

Installation

Install the core package, plus any output plugins you need.

The simplest call (QuickPalette.resolve)

For basic use cases, you don't even need to configure an engine. Use the QuickPalette.resolve static method:

One import, one call. No schema to define, no pipeline to declare. The framing argument ('dark' or 'light') picks the clamp envelopes, and everything else uses sensible defaults.

2. The Four Stages

Every iridis pipeline passes through four conceptual stages, even though the task names and execution order are fully controlled by you:

The Data Flow

Each stage reads from and writes to a shared, mutable state object — a task's own manifest documents exactly which reads and writes it touches:

enforce tasks nudge foreground colors until every contrast pair meets its required ratio; emit tasks aren't part of palette-building itself — they run afterward, writing the resolved state into consumer-shaped output such as CSS variables, a VS Code theme JSON, or Capacitor status bar parameters.

TaskRegistry, the Spine

A TaskRegistry is a Map<string, TaskInterface>. Every task has a string name; the engine owns one registry instance, and calling engine.pipeline([...]) with an ordered list of stage names validates that every name is registered before storing the order, then executes them in that order during engine.run(). Lifecycle hooks (onRunStart, onRunEnd) let plugins initialize or flush state without occupying a pipeline slot.

For every registered, non-lifecycle task named by manifest.requires, the engine validates that the required task is present earlier in the pipeline. A missing or out-of-order predecessor fails during engine.pipeline(). Callers and plugins remain responsible for registering tasks before declaring the sequence; reads and writes document state flow but do not create dependencies automatically.

Plugins Bring the Optional Stages

Core intake/resolve/emit stages ship with iridis itself. Optional enforce stages — WCAG AA/AAA, APCA, and CVD simulate — are contributed by plugins such as @studnicky/iridis-contrast, switched on or off per project.

A plugin is any object satisfying PluginInterface (a name, a version, and a tasks() method); engine.adopt(plugin) registers all of a plugin's tasks in one call. iridis ships seven plugins beyond the core task set: @studnicky/iridis-vscode, -stylesheet, -tailwind, -image, -contrast, -capacitor, and -rdf. Each is a separate package — install only what a project needs.

Upload

Upload

Extract a palette from one or more images — upload them or try a sample. Each image is decoded and reduced to its own dominant colors independently, with its own extraction settings; the Combine stage right after this one merges every image's result into one final palette.

Drop image(s) or click to browse
PNG, JPG, WEBP — each image is extracted independently, then combined in the next stage

Refine

Palette

Seed colors entered here feed the engine directly.

Palette

The Intake stage's picker seeds — assign each hue a role below.

#000000
#ffffff
CVD vision
Color vision deficiency (CVD) checking runs on every palette, always — see enforce:cvdSimulate in the Pipeline card. Auto-correct below adjusts the palette itself; the preview toggles further down only change how this page looks to you — they never touch the palette.
Auto-correct CVD failuresAlways-on — adjusts the palette itself, not just a preview.
Simulate CVD vision

The L-cone (long-wavelength, red-sensitive) is absent. Reds appear darker and can be confused with black, greens, or browns.

~1% of men
Protanopia

The M-cone (medium-wavelength, green-sensitive) is absent — the most common dichromacy. Reds and greens both shift toward a shared yellowish-brown.

~1% of men
Deuteranopia

The S-cone (short-wavelength, blue-sensitive) is absent. Rare, and unlike the other two, affects men and women about equally. Blues and greens, or yellows and violets, become hard to tell apart.

<0.01% of people
Tritanopia

Complete absence of color vision (rod monochromacy) — everything resolves to luminance only, the way a black-and-white photo does.

very rare
Achromatopsia
35 warnings

Pick any combination above — real CVD isn't always one condition, and previewing multiple at once layers their filters together.

Schema

Role schema

How many roles to resolve — iridis-4 is the minimal set, iridis-32 resolves the full token surface this site renders.

Color Space

The color space used when exporting CSS variables.

Display P3 allows for much wider gamut colors on compatible displays.

Compliance strictness

APCA is the modern perceptual contrast algorithm (target Lc).
AAAAAAPCA
Derivation Relations

Every derived role's hue relation to its parent, grouped by hub. Picking an algorithm here changes what expand:family actually derives — not a preview.

Semantic hue nudge

Independent of the relations below — derive:semanticHues nudges success/warning/error/info toward their conventional meaning, bounded to ±90° so a role never jumps to a hue absent from your actual palette (e.g. a red-dominant image still yields a warm-leaning, not pure-green, success). Turn it off to let those 4 roles resolve purely from their own seed/relation, with no built-in lean.

  • Error → 25° (orange)
  • Info → 230° (blue)
  • Success → 160° (green)
  • Warning → 60° (yellow)

Text

2 derived roles
text-strong
Base (0°)
text-subtle
Base (0°)

Brand

17 derived roles
syntax-number
+60°
syntax-string
+120°
link-hover
-15°
syntax-class
+30°
syntax-type
-150°
syntax-operator
-60°
syntax-keyword
Base (0°)
warning
+85°
syntax-function
+180°
success
+120°
syntax-attribute
-90°
syntax-tag
-120°
link
-15°
focus-ring
Base (0°)
accent-alt
+150°
info
-60°
on-brand
Base (0°)

Muted

2 derived roles
syntax-punctuation
Base (0°)
syntax-comment
Base (0°)

Background

3 derived roles
divider
Base (0°)
bg-soft
Base (0°)
surface
Base (0°)

Divider

2 derived roles
border-strong
Base (0°)
border
Base (0°)

Bg-soft

1 derived role
code-bg
Base (0°)

Explore

Roles table

Same roles as Resolved roles, laid out for contrast scanning — sort by any field below.

33 roles

AAA exceeds 7:1 · AA meets the pair's own WCAG target · fail falls short · n/a a structural role (border, surface, code background…) never meant to be read as text against this background.

1. Compliance2. Ratio
text
Ratio 18.76
AAA
text-strong
Ratio 18.44
AAA
link-hover
Ratio 10.58
AAA
brand
Ratio 10.49
AAA
warning
Ratio 9.37
AAA
error
Ratio 9.25
AAA
success
Ratio 8.89
AAA
link
Ratio 7.68
AAA
text-subtle
Ratio 7.30
AAA
muted
Ratio 7.28
AAA
accent-alt
Ratio 6.88
AA
info
Ratio 6.46
AA
divider
Ratio 3.03
AA
syntax-number
Ratio 11.16
n/a
syntax-string
Ratio 10.75
n/a
syntax-class
Ratio 10.47
n/a
syntax-type
Ratio 9.76
n/a
syntax-operator
Ratio 9.63
n/a
syntax-keyword
Ratio 9.44
n/a
syntax-function
Ratio 9.34
n/a
syntax-attribute
Ratio 8.76
n/a
syntax-tag
Ratio 8.60
n/a
focus-ring
Ratio 7.13
n/a
syntax-punctuation
Ratio 6.54
n/a
syntax-comment
Ratio 4.14
n/a
border-strong
Ratio 2.14
n/a
border
Ratio 1.42
n/a
bg-soft
Ratio 1.07
n/a
surface
Ratio 1.04
n/a
code-bg
Ratio 1.03
n/a
on-brand
Ratio 1.01
n/a
background
Ratio 1.00
n/a
overlay
Ratio 1.00
n/a

Ratio and Compliance are live WCAG 2.1 measurements, nudged into range by the engine's contrast enforcement — see Math Primitives Reference for the WCAG/APCA math and how `enforce:contrast` corrects a failing pair.

Roles

Same roles as Roles table, laid out with their raw OKLCH values — sort by any field below.

33 roles
1. Compliance2. Ratio
text
#f2f2f2
L 0.96C 0.00H 90°
text-strong
#f4f0e5
L 0.96C 0.01H 90°
syntax-number
#17d967
L 0.77C 0.21H 150°
syntax-string
#2fcbe1
L 0.77C 0.20H 210°
link-hover
#f5a925
L 0.79C 0.23H 75°
brand
#d5b455
L 0.78C 0.12H 90°
syntax-class
#a7c31f
L 0.77C 0.20H 120°
syntax-type
#c1a1fb
L 0.77C 0.16H 300°
syntax-operator
#fc9382
L 0.77C 0.16H 30°
syntax-keyword
#d2a924
L 0.75C 0.21H 90°
warning
#fb942a
L 0.76C 0.22H 60°
syntax-function
#8faafd
L 0.75C 0.18H 270°
error
#fb8d85
L 0.76C 0.18H 25°
success
#18c080
L 0.72C 0.21H 160°
syntax-attribute
#fc7faa
L 0.75C 0.18H 360°
syntax-tag
#f178e9
L 0.75C 0.20H 330°
link
#d28f11
L 0.70C 0.21H 75°
text-subtle
#9d988b
L 0.68C 0.02H 90°
muted
#989898
L 0.68C 0.00H 90°
focus-ring
#b7920f
L 0.68C 0.27H 90°
accent-alt
#059ce4
L 0.66C 0.20H 240°
syntax-punctuation
#998f74
L 0.65C 0.04H 90°
info
#1ba08d
L 0.64C 0.22H 180°
syntax-comment
#756e5b
L 0.54C 0.03H 90°
divider
#6a545a
L 0.47C 0.03H 0°
border-strong
#573c44
L 0.39C 0.04H 0°
border
#352328
L 0.28C 0.03H 0°
bg-soft
#1a050d
L 0.16C 0.04H 0°
surface
#100307
L 0.13C 0.03H 0°
code-bg
#0b0305
L 0.12C 0.02H 0°
on-brand
#030201
L 0.09C 0.01H 90°
background
#000000
L 0.04C 0.00H 0°
overlay
#000000
L 0.02C 0.00H 0°
Pairings

Sample text/background pairings drawn from the resolved roles above, so you can see how the palette actually reads as UI.

3 pairings

Light on dark

The quick brown fox jumps over the lazy dog.

text on overlay18.8:1 — AAA pass

Dark on light

The quick brown fox jumps over the lazy dog.

background on text18.8:1 — AAA pass

Lower contrast

The quick brown fox jumps over the lazy dog.

syntax-comment on text-strong4.5:1 — AA pass
Spectrum

The full 50→950 ramp per alias.

1. Compliance2. Ratio
Success
50↔100: 1.07100↔200: 1.10200↔300: 1.18300↔400: 1.31400↔500: 1.40500↔600: 1.42600↔700: 1.47700↔800: 1.50800↔900: 1.40900↔950: 1.42
500
Info
50↔100: 1.07100↔200: 1.10200↔300: 1.19300↔400: 1.31400↔500: 1.40500↔600: 1.44600↔700: 1.46700↔800: 1.48800↔900: 1.40900↔950: 1.42
500
Secondary
50↔100: 1.09100↔200: 1.12200↔300: 1.20300↔400: 1.30400↔500: 1.39500↔600: 1.43600↔700: 1.45700↔800: 1.48800↔900: 1.39900↔950: 1.40
500
Neutral
50↔100: 1.09100↔200: 1.13200↔300: 1.21300↔400: 1.30400↔500: 1.39500↔600: 1.42600↔700: 1.46700↔800: 1.47800↔900: 1.39900↔950: 1.40
500
Primary
50↔100: 1.09100↔200: 1.14200↔300: 1.21300↔400: 1.31400↔500: 1.39500↔600: 1.43600↔700: 1.45700↔800: 1.48800↔900: 1.38900↔950: 1.39
500
Warning
50↔100: 1.10100↔200: 1.13200↔300: 1.22300↔400: 1.34400↔500: 1.39500↔600: 1.44600↔700: 1.44700↔800: 1.46800↔900: 1.38900↔950: 1.38
500
Error
50↔100: 1.10100↔200: 1.13200↔300: 1.23300↔400: 1.35400↔500: 1.42500↔600: 1.43600↔700: 1.47700↔800: 1.45800↔900: 1.36900↔950: 1.34
500
Color graph

The resolved role graph, live — every node is that role's own engine.run() color, edges are derivation lineage settling under a force simulation. Pinned/synthesized/direct-match roles toggle via the legend.

Loading graph engine…
Components

Click to fire a real UToast

Dismissible alerts

No alerts yet — add one.

Button variants

solid
soft
outline
ghost

Live form

UTable — top of the current sort (6 of 33 roles)

RoleHexRatioCompliance
text #f2f2f218.76AAA
text-strong #f4f0e518.44AAA
syntax-number #17d96711.16AAA
syntax-string #2fcbe110.75AAA
link-hover #f5a92510.58AAA
brand #d5b45510.49AAA

Dropdown, tooltip & popover

K
Interactables

Switch & radio

Notifications

Checkbox group — filters the avatars/badges card

Avatars & badges — filtered by the checkboxes above

Pprimary
Ssuccess
Eerror

UTabs

33 roles resolved · background #000000 · compliance target APCA.

UAccordion

UPagination — same sorted role list Components' table uses

APCA compliance

0%
Motion

Timing

Drag the duration — every color transition on this page, not just the swatches below, runs on this same clock.

Live engine swatches

These six swatches are engine-computed in real time — the same OKLCH curve evaluation driving the ambient background — not CSS keyframes.

primary
secondary
success
warning
error
info

This card is a live demo of Living Color — the engine's palette-as-animated-vector layer, not just a static derivation. See Living Color for the underlying package (iridis-anima, iridis-pulse, iridis-fsm) that drives every transition here.

Named animation library

pulse-glow · 3s
carousel arrows, active dot
float · 7s
hero logo, floating orbs
spin · 26s
ambient background accent
sheen · 4s
every glass panel’s top edge
orbit · 2.2-3.8s
three roles, three independent rings
sonar · 2.4s
success/warning/error/primary in sequence
radar · 2.6s
primary bleeding into secondary, one sweep
chroma · 4s
the accent hue cycling the full wheel
Color stream

Six independent seismograph strips — one per decorative role — tracing that role's live chroma drift, oldest at the left, newest at the right, each segment colored by its own computed hex. Beneath each, a static comparison: a naive RGB channel lerp against the engine's OKLCH lerp, both spanning the same complementary hue swing — the naive band often dips through a muddy, desaturated midpoint that the OKLCH band avoids.

This is Living Color in motion — see Living Color for the palette-vector math and curve-evaluation packages behind the drift you're watching.

Error — engine-driven (live)
CSS lerp (naive RGB)
OKLCH engine
Info — engine-driven (live)
CSS lerp (naive RGB)
OKLCH engine
Primary — engine-driven (live)
CSS lerp (naive RGB)
OKLCH engine
Secondary — engine-driven (live)
CSS lerp (naive RGB)
OKLCH engine
Success — engine-driven (live)
CSS lerp (naive RGB)
OKLCH engine
Warning — engine-driven (live)
CSS lerp (naive RGB)
OKLCH engine

Stylesheets

CSS variables

Drop this in your project's global stylesheet — it defines the CSS custom properties every component reads from.

theme.css
CSS vars (scoped)

Drop this in your project's global stylesheet — the same variables, scoped under a selector instead of :root.

theme.scoped.css
Tailwind

Drop this in your project root as tailwind.config.ts — it extends Tailwind's theme with the resolved palette.

tailwind.config.ts
shadcn/ui

Drop this in your project's globals.css — shadcn/ui's components read these CSS variables directly.

globals.css
MUI

Drop this in your project as theme.ts and pass it to MUI's ThemeProvider.

theme.ts
Chakra UI

Drop this in your project as theme.ts and pass it to Chakra's ChakraProvider.

theme.ts
Panda CSS

Drop this in your project root as panda.config.ts — Panda CSS reads the theme tokens from here.

panda.config.ts
UnoCSS

Drop this in your project root as uno.config.ts — UnoCSS reads the theme tokens from here.

uno.config.ts
Capacitor

Drop this in your project as capacitor.theme.ts — Capacitor's status bar and splash screen read these values.

capacitor.theme.ts
Android theme.xml

Drop this in your Android project's res/values/themes.xml — the native shell reads these values.

themes.xml
JSON

Drop this in your project as theme.json — every other format on this carousel is derived from this same resolved data.

theme.json
RDF (Turtle)

Drop this in your project as theme.ttl — the resolved roles expressed as RDF/OWL triples for semantic tooling.

theme.ttl
VS Code theme

Drop this in your VS Code extension's themes/ folder and reference it from package.json's contributes.themes.

theme-color-theme.json

Reference

Pipeline

Every useful iridis pipeline passes through four conceptual stages — intake → resolve → enforce → emit — even though the task names and order below are yours to define. See The Four Stages for the data flow, the registry, and how plugins contribute optional stages.

Expand a stage — the description underneath is that task's own manifest, not marketing copy. Optional stages are automatically switched on or off depending on the compliance strictness setting.

Intake

required

Resolve

required

required

required

required

required

required

Enforce

required

disabled

disabled

enabled

required

Schema tree

Each tier adds roles over the last. "resolved" competes for a seed by default; "← source" is hue-derived — pin either one in Palette to override it.

1. Compliance2. Ratio

Each leaf above is one RoleDefinitionInterface entry — a named, intent-classified contract, not a raw color. See Long-form Engine API for what every field does.

Hue derivation

Every non-monochromatic role in Derivation Settings picks its hue via one of these 8 algorithms, applied to a seed hue. Pick a seed role and watch all 8 update together.

#b8941d
90°
Monochromatic
1 hue
No hue shift — every derived role reads as the same hue as the seed.
Complementary
2 hues
One hue sitting exactly 180° from the seed, on the opposite side of the wheel.
Analogous
3 hues
The seed plus two neighbours, spaced evenly on either side.
Triadic
3 hues
Three hues spaced 120° apart — an equilateral triangle around the wheel.
Tetradic
4 hues
Four hues spaced 90° apart — a square around the wheel.
Split-complementary
3 hues
The seed plus the two hues neighbouring its complement, rather than the complement itself.
Compound
6 hues
Analogous around the seed AND analogous around its complement — six hues total.
Freeform
3 hues
User-specified hue offsets, set per role in Derivation Settings below — shown here with an illustrative default.

Clamps

This shows the engine's hidden "hand" by revealing how every role was selected from the provided seeds. You can see the OKLCH distance for each candidate seed, whether a role had to be synthesized, and where input seeds were forcefully clamped to satisfy lightness, chroma, or semantic hue envelopes.

1. Compliance2. Ratio

3. Adopting Iridis In An Existing App