Why a custom engine, and how it works
Research ahead of the app redesign found that Bibel TV users associate the brand with hope, and expected that in the interface itself, not just the content — against the industry's own dark-mode default — a considered choice, not a habit: a dark frame recedes the way a theater dims its walls, so artwork and video carry the screen instead of the interface competing with them. Taking that seriously meant the colour engine had to prove itself in light mode first, the reverse of how a streaming product's theming normally gets built and tuned.
The first attempt wasn't a custom engine at all. Bibel TV already runs its images through imgix, and imgix ships with a palette-extraction feature. So we used it, and looked at what came out across the catalog.
There were two problems. The first was visible at a glance: too often the colors collapsed. In dark mode the catalog drifted toward beige and brown, in light mode toward pink and rose, different artworks ending up with too-similar themes. The second problem was less obvious in any one card but unmistakable across a grid: the colors weren't perceptually uniform. Some cards came back heavy and saturated, others washed out and weak, and the difference had no relationship to the artwork itself. Scrolling the grid was noisy: some cards demanded the eye, others almost disappeared, no rhythm to any of it. That uneven weight is the real reason the catalog stopped feeling like a coherent collection.
The diagnosis for both failures was the color space. RGB isn't perceptually uniform: two colors that are mathematically far apart can read as nearly the same shade to a human, and two colors that are mathematically close can read as very different visual weights. An RGB extractor has no way to control for either, so it produces both monotone drift and uneven weighting at the same time.
So I built the engine in OKLCH, the perceptual color space: where lightness and chroma map to how humans actually see, not to how RGB numerically partitions a cube. The win in OKLCH isn't theoretical: you can clamp output to a band of perceived lightness so every card lands in the same visual weight range, and reason about chroma directly so cards stay distinct without one demanding the page and another disappearing.
The algorithm is short. K-means clustering finds the most prominent colors in the artwork; a hue-and-vibrance filter drops the clusters that are dull or neutral, so muddy beiges never win just by covering the most pixels. The remaining clusters are mapped to roles: base, accent, text. Text colors are validated against the base using the WCAG contrast algorithm; if the pair fails, lightness and chroma are adjusted until it passes. The page-background gradient is computed from the base color at reduced opacity, so the page feels like it belongs to the content.
From a single piece of artwork, the API returns a full theme: a base color anchoring the card gradient, an accent for the logo, one or two text colors that clear AA or AAA contrast against the base without anyone checking.
The engine is tuned through Color Lab, the internal configuration tool shown above. The normalization curves and hue-exclusion gradients on the left of that screen are how the extraction is calibrated for Bibel TV specifically; the system is bespoke to one brand, not a generic per-brand tool. The two phone previews on the right show the same content card rendered in dark and light mode from the same extracted palette.
What changed
- Light mode preference
- A concept survey (n=264) found 51% of respondents with a clear preference chose light outright and 32% chose a mixed palette — only 17% preferred dark alone, against the current app's dark-only default
- Contrast compliance
- Every extracted theme validated at AA or AAA, no manual checking
- Delivery
- Live in production. Color data is written onto every image in the content library, so each artwork ships with its theme pre-computed. The engine is also accessible through a Figma plugin when designers are working with those images.

What I took from it
Infrastructure work is design work. The thresholds and filters inside the extraction algorithm are design decisions, and when you hand them to a generic library you hand over brand quality with them.
The other lesson was restraint. The engine can produce complex multi-tone looks. On a phone, the simpler output was the better one. Sometimes the design decision is to not use what you built.
Read the full case study
How it works
Load the artwork
Content image loaded from a URL or upload, converted to a pixel array.
Cluster in OKLCH
K-means finds the most prominent colors, working in OKLCH so proximity matches human vision, not just math.
Filter for character
Low-vibrance clusters are dropped, so the theme is built from colors that actually define the artwork.
Map to roles
Clusters are assigned to base, accent, and text roles, and the page-background gradient is computed from the base color.
Validate contrast
Text colors are checked against the base with the WCAG algorithm. Lightness and chroma are adjusted until the pair passes.
Return the theme
A JSON theme, colors in hex and OKLCH, gradient config, contrast ratios, ready to use directly in app theming.
The decisions that mattered
Design owned the infrastructure
Color extraction usually lives with platform engineering. I owned it, because the choices inside it — hue thresholds, vibrance filters, perceptual versus mathematical proximity — are design decisions. Delegate them to a generic API and you delegate brand quality.
Tunable by design, not by code
Color Lab exists to keep the engine flexible. Calling it a debugging tool undersells it. K-means thresholds, hue-exclusion ranges, light- and dark-mode normalization curves, all of it lives behind dials in the config UI, not behind code commits. The dials control both the underlying logic and the resulting appearance, anywhere from soft pastels to rich and vibrant.
We don't yet know where the catalog feels best across the board. Early user feedback was that vibrant is too much, so we're shipping with a middle-of-the-road palette and watching how it lands. That's the point of building the config layer in the first place: we can keep adjusting as the data and the feedback come in, without an engineer in the loop.
Light mode came first
Bibel TV's brand is warm, hopeful, light. Users associate it with brightness, not a cinema black-out. So the pipeline was tuned for light-mode correctness first, then verified in dark mode, the reverse of the streaming-app norm.
Restraint on the phone
Bibel TV's 2025 on-air rebrand uses two text colors for visual hierarchy. The engine produces both. On a dense phone display the second text color tested as too much color, validated and shipped with one. The capability is kept in the engine for other surfaces where it still makes sense: web, tablet, or even on-air itself, where color picking is currently a manual process. The mobile call was not 'drop the feature' but 'don't use it here'.
The app should feel like the broadcast
The card gradient and the page gradient are digital translations of the on-air color language. Every piece of artwork produces a theme that feels like Bibel TV, not like Netflix with a different logo, and it happens automatically, with no manual color matching.
Curious why a color engine built for 10,000+ videos got tuned for light mode before dark, the reverse of the usual streaming default, or why the brand's own two-color hierarchy ships with only one color on phones, on purpose? Get in touch.