<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Andrey Yamanov</title>
    <description>The latest articles on DEV Community by Andrey Yamanov (@tenphi).</description>
    <link>https://dev.to/tenphi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F333753%2Fade0c743-2699-43f4-a6c2-adcf5ae2bede.jpeg</url>
      <title>DEV Community: Andrey Yamanov</title>
      <link>https://dev.to/tenphi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tenphi"/>
    <language>en</language>
    <item>
      <title>Runtime CSS-in-JS deserves a rehearing</title>
      <dc:creator>Andrey Yamanov</dc:creator>
      <pubDate>Wed, 07 Oct 2026 12:30:02 +0000</pubDate>
      <link>https://dev.to/tenphi/runtime-css-in-js-deserves-a-rehearing-4hl2</link>
      <guid>https://dev.to/tenphi/runtime-css-in-js-deserves-a-rehearing-4hl2</guid>
      <description>&lt;p&gt;Runtime CSS-in-JS has largely been written off as too expensive for the browser.&lt;/p&gt;

&lt;p&gt;The reasons are familiar: some libraries rebuilt styles on every React render, interleaved stylesheet writes with rendering, accumulated generated CSS, and added React overhead to every styled element. Server rendering was awkward, React Server Components seemed incompatible, and build-time CSS appeared to eliminate the entire class of problems.&lt;/p&gt;

&lt;p&gt;Those concerns did not come from nowhere. In 2019, Aggelos Arvanitakis described &lt;a href="https://calendar.perfplanet.com/2019/the-unseen-performance-costs-of-css-in-js-in-react-apps/" rel="noopener noreferrer"&gt;the hidden costs of the CSS-in-JS libraries of the time&lt;/a&gt;. In 2022, Emotion maintainer Sam Magura reported in &lt;a href="https://dev.to/srmagura/why-were-breaking-up-wiht-css-in-js-4g9b"&gt;Why We're Breaking Up with CSS-in-JS&lt;/a&gt; that replacing Emotion with Sass Modules cut the render time of a large screen almost in half.&lt;/p&gt;

&lt;p&gt;The measurements were valid. They answered, “Were these runtime CSS-in-JS implementations expensive?” They left two broader questions open: “Must every runtime-capable styling system pay those costs in the browser?” and “When generation does happen there, does the resulting flexibility justify the cost?”&lt;/p&gt;

&lt;p&gt;The same concerns drive more recent migrations. &lt;a href="https://github.blog/engineering/architecture-optimization/improving-site-performance-by-shipping-more-css/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; replaced styled-components with CSS Modules to reduce server and client costs; &lt;a href="https://linear.app/now/styling-linear-for-the-future-stylex" rel="noopener noreferrer"&gt;Linear&lt;/a&gt; moved to StyleX for performance and clearer component boundaries; &lt;a href="https://community.developer.atlassian.com/t/rfc-73-migrating-our-components-to-compiled-css-in-js/85953" rel="noopener noreferrer"&gt;Atlassian&lt;/a&gt; adopted Compiled to reduce runtime overhead and support concurrent rendering and streaming SSR.&lt;/p&gt;

&lt;p&gt;Those choices strengthen the case for generating known styles early and making styling contracts explicit. Improvements in browsers, React, rendering frameworks, and hardware help, but the stronger reason to revisit the verdict is architectural: style engines can change where they perform work and how often they repeat it. Linear still generates theme rules at runtime, showing how that capability can remain useful within a mostly compiled system. A runtime-capable engine has to justify both the work it leaves in the browser and the freedom it gives consumers to restyle shared components.&lt;/p&gt;

&lt;p&gt;People often use &lt;em&gt;runtime CSS-in-JS&lt;/em&gt; as shorthand for generating CSS in the browser. Here I separate capability from placement. A runtime-capable system can generate styles wherever its inputs become known: during the build, on the server, or in the browser.&lt;/p&gt;

&lt;p&gt;The useful question is not simply whether runtime CSS-in-JS is fast or slow. It is which styling decisions can be made earlier, and whether keeping the rest open justifies the cost. CSS should not always be generated in the browser, but browser generation should remain available when the final composition genuinely remains open.&lt;/p&gt;

&lt;p&gt;I will use &lt;a href="https://tasty.style" rel="noopener noreferrer"&gt;Tasty&lt;/a&gt;, which I created and maintain, as a case study rather than a neutral survey. It is a demanding test: in the browser, Tasty parses values, resolves states and conditions, composes extensions, and generates and injects CSS rather than merely selecting precompiled output. The same components can do that work on a server or during a static build.&lt;/p&gt;

&lt;p&gt;My claim is narrow: a runtime-capable system can justify its cost when it controls where and how often that cost is paid and solves composition problems that matter to the product. That does not make every CSS-in-JS library fast or make browser generation free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why leave composition open?
&lt;/h2&gt;

&lt;p&gt;A changing value alone is a weak reason to generate CSS at runtime. When the declaration structure is fixed, a CSS custom property is often more direct. Runtime generation becomes useful when some part of the structure—the property set, selector, condition, state, or component extension—remains unknown until the program runs. &lt;em&gt;Open&lt;/em&gt; means that the program can introduce a combination that was not enumerated before deployment.&lt;/p&gt;

&lt;p&gt;The reason is often ownership: a component and its consumer own different styling decisions. The component owns its appearance and internal layout; the consumer decides how it participates in the surrounding page. A reusable component cannot predict every layout, state, or product-specific extension it will encounter.&lt;/p&gt;

&lt;p&gt;In my work at Cube, that boundary became a delivery problem. The component library and the product could not always evolve at the same pace. When a developer needed a styling capability that Cube UI Kit did not expose, the maintainable long-term path was to add a component API first. Product work could not always wait, so developers added local CSS overrides and JavaScript conditions. When the UI Kit eventually gained the capability, those workarounds still had to be found and migrated.&lt;/p&gt;

&lt;p&gt;Tasty made those workarounds explicit without making the direct &lt;code&gt;styles&lt;/code&gt; prop the preferred API. The preferred fix depends on what is missing. A component can expose individual CSS properties such as &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;border&lt;/code&gt;, or &lt;code&gt;radius&lt;/code&gt; through &lt;code&gt;styleProps&lt;/code&gt;. With &lt;code&gt;modProps&lt;/code&gt;, it can expose a finite component state or visual variant as a typed prop that selects predefined style branches. New behavior, semantics, and accessibility require dedicated component API props. The direct &lt;code&gt;styles&lt;/code&gt; prop remains a poor default and an escape hatch for rare cases that can be justified locally.&lt;/p&gt;

&lt;p&gt;When a visual extension remains product-specific, a developer can create a named styled wrapper in the same language as the component library. If the need proves reusable, the wrapper can remain a product-local component or the capability can move into the UI Kit as an exposed style, a state modifier, or a dedicated API. Migration means moving an explicit declaration instead of untangling JavaScript conditions and CSS overrides. Product development and design-system development no longer have to happen in lockstep.&lt;/p&gt;

&lt;p&gt;Within that hierarchy, style definitions remain composable one property at a time. Suppose a &lt;code&gt;Button&lt;/code&gt; already exposes a &lt;code&gt;loading&lt;/code&gt; state modifier and defines &lt;code&gt;fill&lt;/code&gt; for its default, hover, and disabled states. A product-local wrapper can add the missing &lt;code&gt;loading&lt;/code&gt; branch without copying or replacing that state map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LoadingButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tasty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#yellow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tasty keeps the existing &lt;code&gt;fill&lt;/code&gt; states and adds &lt;code&gt;loading&lt;/code&gt;. The product does not have to restate the existing states or create a separate CSS rule that wins through the cascade.&lt;/p&gt;

&lt;p&gt;This hierarchy does not prevent exceptions from accumulating; it makes them explicit and movable. Teams still have to decide which should graduate into shared APIs.&lt;/p&gt;

&lt;p&gt;The same ownership boundary appears in ordinary layout. Whether a component stretches, aligns itself, fills available space, or sits against an edge is normally decided where it is used. A general-purpose &lt;code&gt;Grid&lt;/code&gt; cannot know whether its consumer needs equal columns, a fixed sidebar, named areas, or a template assembled from application data.&lt;/p&gt;

&lt;p&gt;A design system can represent those decisions with style props, state modifiers, utilities, custom properties, or wrappers. Restricting the styling surface can be intentional. The tradeoff is that each unpredicted need requires another narrow contract or escape hatch.&lt;/p&gt;

&lt;p&gt;Tasty instead acts as a lazy compiler. It receives the concrete styling decision where the component is used and generates the requested CSS. It does not eliminate abstraction; it offers one shared language in place of many APIs that each have to predict their future use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime capability does not dictate runtime placement
&lt;/h2&gt;

&lt;p&gt;Earlier runtime libraries often connected style generation directly to React rendering. A styled element might run hooks, consume theme context, rebuild a complete class, and insert its rule immediately. Server support then had to reproduce a browser-oriented system in a different environment.&lt;/p&gt;

&lt;p&gt;None of those choices is required by CSS-in-JS. A style engine can cache work below the component level, reuse output across components, collect writes until React reaches the insertion phase, and remove rules that are no longer used. React Server Components and build-time rendering also let compatible components produce HTML and CSS without sending their styling runtime to the browser.&lt;/p&gt;

&lt;p&gt;The styling language and its execution point are separate architectural decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  During the build
&lt;/h3&gt;

&lt;p&gt;When the complete set of styles is known, Tasty can move generation out of the browser in two ways. &lt;code&gt;tastyStatic&lt;/code&gt; analyzes application code, writes a CSS file, and leaves class names in the code. A framework that renders pages at build time can instead run regular Tasty components and collect the CSS they produce. Either approach lets a fully static page ship without the Tasty runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  On the server
&lt;/h3&gt;

&lt;p&gt;Tasty components can run as React Server Components without adding a &lt;code&gt;'use client'&lt;/code&gt; boundary. On a server-only page, they produce HTML and CSS without sending the Tasty runtime to the browser.&lt;/p&gt;

&lt;p&gt;In an interactive server-rendered application, Tasty collects CSS while React renders and can stream it with the page. It also records which classes were generated, so the client can reuse them during hydration instead of generating and injecting them again.&lt;/p&gt;

&lt;p&gt;With that handoff, server rendering avoids both a flash of unstyled content and duplicated client work. It requires an implementation designed for the server rather than a client-side injector retrofitted after the fact.&lt;/p&gt;

&lt;h3&gt;
  
  
  In the browser
&lt;/h3&gt;

&lt;p&gt;Browser generation remains useful when styling values or combinations are still unknown after the build and server render. The browser pays for genuinely new styles, but it can reuse work it has already seen.&lt;/p&gt;

&lt;p&gt;These placements are not hypothetical. &lt;a href="https://github.com/cube-js/cube-ui-kit" rel="noopener noreferrer"&gt;Cube UI Kit&lt;/a&gt; generates styles in the browser; &lt;a href="https://github.com/tenphi/tasty.style" rel="noopener noreferrer"&gt;tasty.style&lt;/a&gt; does so during React Server Component rendering; &lt;a href="https://github.com/tenphi/tenphi.me" rel="noopener noreferrer"&gt;tenphi.me&lt;/a&gt; does so during Astro's static build. They demonstrate that each placement is viable, not that one always wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  What static extraction must be able to see
&lt;/h2&gt;

&lt;p&gt;Static tooling can provide the same composition when it can see all the inputs. A closed styling vocabulary can be a deliberate and useful design-system constraint. The tradeoff appears when the product needs a combination outside that vocabulary.&lt;/p&gt;

&lt;p&gt;Consider a form renderer that chooses both a component and its extension from a registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fieldTypes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Component&lt;/span&gt; &lt;span class="na"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To reproduce this composition exactly, an extractor has to trace every possible &lt;code&gt;Component&lt;/code&gt; back to its original style declaration. It must also find every possible extension and reproduce how each combination merges. That works when &lt;code&gt;fieldTypes&lt;/code&gt; is local, finite, and statically visible, and when the schema selects only known entries. The assumption breaks once a package or plugin extends the registry, or application or server configuration supplies additional styles.&lt;/p&gt;

&lt;p&gt;Static tooling can accommodate each of those patterns. A team can enumerate variants, safelist output, add compiler annotations, route values through custom properties, or constrain how components are composed. But each accommodation makes source visibility part of the application architecture: every wrapper, registry, package boundary, and data-driven abstraction has to remain legible to the extractor.&lt;/p&gt;

&lt;p&gt;Runtime generation is not needed for ordinary static declarations. It is useful when the final combination does not exist until the program runs. At that point, the component and its styles have already met; the engine can combine their concrete values without reconstructing how they traveled through the source.&lt;/p&gt;

&lt;p&gt;That visibility requirement extends through the toolchain. The compiler has to participate in every path that handles uncompiled styling code: application builds, unit-test transforms, Storybook, browser-test bundles, separately built component libraries, and sometimes the build of every consuming application. Each path needs compatible compiler and styling configuration.&lt;/p&gt;

&lt;p&gt;A library can publish compiled class names and CSS instead, but then it owns CSS delivery. One global stylesheet can force every consumer to load every component's styles. Splitting the output requires the compiler and bundler to agree on which CSS belongs to each module or route, and the generated CSS, JavaScript class names, caches, and package versions must remain in sync.&lt;/p&gt;

&lt;p&gt;Static extraction may still be the right choice, but zero browser runtime is not zero system cost. Too often, browser work is quantified while source restrictions, build integration, and CSS delivery remain implicit. These costs are not interchangeable—runtime work is paid by users, while tooling and coordination are paid by teams—but every placement should be judged by the costs it creates and the problems it solves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser costs that remain
&lt;/h2&gt;

&lt;p&gt;When the final styling decision reaches the browser, four areas still matter: generation and reuse, stylesheet writes, stylesheet growth, and React wrapper work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generation and reuse
&lt;/h3&gt;

&lt;p&gt;When a Tasty component renders with the same styles, it remembers the class names it produced and skips generation. Dynamic styles are divided into reusable chunks such as layout, dimensions, typography, and appearance. If only padding changes, Tasty reuses the other chunks and generates only the new spacing chunk; another component can reuse that chunk too.&lt;/p&gt;

&lt;p&gt;The parser and style pipeline have their own caches, and identical state branches can be collapsed before output is generated. Repeated work stops at the earliest layer that recognizes it. A React render therefore need not trigger another round of parsing, generation, or insertion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stylesheet writes and style resolution
&lt;/h3&gt;

&lt;p&gt;Tasty normally keeps generated component rules in one managed &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; element per root, opening another only when the configured limit is reached. The main cost is not creating elements but mutating a live stylesheet. How much work can be grouped depends on when the browser resolves those changes.&lt;/p&gt;

&lt;p&gt;Adding a rule marks styles as needing resolution, but the browser does not necessarily recalculate the page at that moment. It can collect several changes and resolve them together when styles are needed. React may split rendering into parts, however, giving the browser several opportunities to resolve styles if stylesheet writes are interleaved with reads.&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://github.com/tenphi/tasty/blob/299eec7e2aae62a8aa940190dc77fde82bde9ac8/docs/configuration.md#batched-injection" rel="noopener noreferrer"&gt;batched injection&lt;/a&gt; enabled, Tasty's &lt;code&gt;TastyBatchProvider&lt;/code&gt; holds new rules while components render. It applies them in React's &lt;a href="https://react.dev/reference/react/useInsertionEffect" rel="noopener noreferrer"&gt;&lt;code&gt;useInsertionEffect&lt;/code&gt;&lt;/a&gt;, before layout effects can measure the page. This gives the browser an opportunity to resolve the writes together at a predictable point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stylesheet growth
&lt;/h3&gt;

&lt;p&gt;Generated styles can accumulate, especially when every combination of props produces another complete class.&lt;/p&gt;

&lt;p&gt;Tasty limits that growth by reusing chunks and deduplicating identical output. During browser idle time, &lt;a href="https://github.com/tenphi/tasty/blob/299eec7e2aae62a8aa940190dc77fde82bde9ac8/docs/injector.md#garbage-collection" rel="noopener noreferrer"&gt;its collector&lt;/a&gt; removes old rules that are no longer present on the page after a short grace period. The delay matters because React may generate a class before committing its element; deleting every temporarily absent class could remove a rule during a render.&lt;/p&gt;

&lt;p&gt;Unique styles that remain active still grow the stylesheet. Applications with highly dynamic styling should measure that workload. Accumulation does not disappear, but an engine can manage it rather than treating unbounded growth as inevitable.&lt;/p&gt;

&lt;h3&gt;
  
  
  React wrapper work
&lt;/h3&gt;

&lt;p&gt;Older implementations often made every styled component run hooks or consume theme context. Even a simple element paid that cost whenever React rendered it.&lt;/p&gt;

&lt;p&gt;Tasty does not need hooks or React context to generate styles. Cached styles remove the generation work, but the wrapper still processes props and returns the underlying element. That React work remains.&lt;/p&gt;

&lt;p&gt;Browser execution also means shipping the Tasty runtime. Its current bundle is substantial because it includes parsing, state and condition handling, composition, injection, and collection. Tasty deliberately prioritizes a rich composition model and predictable execution; reducing download and parse cost remains optimization work. Products that do not need that capability have less reason to pay for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practical cost
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Updated September 22, 2026: refreshed the benchmarks and product profile after the optimizations in Tasty 3.9.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Generation, injection, and wrapper overhead are different costs. The useful distinction is between cold and repeated work: new styles pay for generation and injection, while familiar styles take the cached path. The figures below measure those costs separately.&lt;/p&gt;

&lt;p&gt;Tasty's latest published benchmarks report three consecutive runs on an Apple M3 Pro, with Node 24.18.0 on AC power for the style pipeline and production React 19.2.8 and Chromium 151 for the browser suites. The earlier figures in this article used an M1 Max and Node 22, so this is an updated snapshot rather than a controlled before-and-after comparison.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measured work&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Render one warmed empty &lt;code&gt;tasty()&lt;/code&gt; wrapper&lt;/td&gt;
&lt;td&gt;~0.5 µs extra per element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generate a new five-property style&lt;/td&gt;
&lt;td&gt;6.3–7.0 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generate a new style with media, hover, and modifier states&lt;/td&gt;
&lt;td&gt;28–32 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reuse a cached style&lt;/td&gt;
&lt;td&gt;~0.10 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generate, inject, and resolve one new rule&lt;/td&gt;
&lt;td&gt;~0.100–0.104 ms extra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generate and inject 1,000 new rules, then resolve styles once&lt;/td&gt;
&lt;td&gt;5.73–5.94 ms extra in total&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These figures describe separate paths and should not be added. Generation and cache timings measure the operation directly; wrapper and injection timings are the extra time over matching baselines. The end-to-end injection benchmark already includes generation and subtracts a baseline that performs the same DOM update and style resolution with equivalent CSS already present. React wrapper work is measured separately: the roughly 0.5 µs figure covers mounting/removing or rerendering with unchanged host props; changing a host attribute raises it to 0.86–0.91 µs per element. The &lt;a href="https://github.com/tenphi/tasty/blob/eaf64ea973855bb394bd48e1aef5406e7b2c6225/docs/runtime-benchmarks.md" rel="noopener noreferrer"&gt;full benchmark report&lt;/a&gt; publishes the methods, results, and source code.&lt;/p&gt;

&lt;p&gt;The pattern matters more than the individual numbers: cached styles avoid most of the work, and grouped writes amortize fixed costs. In these cases, cold generation took a few to a few dozen microseconds, cached reuse took a fraction of a microsecond, and generating, injecting, and resolving a new rule added about a tenth of a millisecond. The 1,000-insertion case added about 55–59 times as much time as the single-rule case, not 1,000 times as much.&lt;/p&gt;

&lt;p&gt;Benchmarks isolate the costs; product profiles show whether they matter in context. After the optimizations in Tasty 3.9, Tasty now accounts for roughly &lt;strong&gt;8.3% of main-thread busy time during startup&lt;/strong&gt; in production Sentry traces from real user sessions on one of Cube's heaviest pages, down from the 12.5% reported here originally. These are observations from ordinary user sessions, not a controlled benchmark or a measure of total page-load time. That share is still material, and whether it is acceptable depends on the product and what the runtime enables.&lt;/p&gt;

&lt;p&gt;In latency-sensitive interactions, especially animation-rich UI, creating many previously unseen styles at once deserves scrutiny. That pattern may also reveal a broader workload problem: too much UI mounting in one frame, poor style reuse, or per-frame values expressed as new rules. Precompilation can remove one source of work, but it does not address React, DOM, layout, or paint costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime is only half the design
&lt;/h2&gt;

&lt;p&gt;A runtime-capable system still has to express the product's CSS. CSS is more than a dictionary of properties: selectors, conditions, declaration order, overlapping shorthands, and the cascade all carry meaning.&lt;/p&gt;

&lt;p&gt;Tasty accepts standard properties and values directly, while its style objects use a small grammar to represent selectors and conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tasty&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:hover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#accent-text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4x&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@media(w &amp;lt; 640px)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2x&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@(sidebar, w &amp;lt; 300px)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1x&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This definition combines a pseudo-class, a viewport condition, and a named container condition. The engine turns them into selectors and at-rules while preserving their declared priority. The same grammar covers features such as &lt;code&gt;:has()&lt;/code&gt;, &lt;code&gt;@supports&lt;/code&gt;, and CSS structures that do not fit into a flat property object.&lt;/p&gt;

&lt;p&gt;Composition needs rules too. CSS properties overlap: &lt;code&gt;padding&lt;/code&gt; can reset &lt;code&gt;paddingTop&lt;/code&gt;, so naive object merging can change the result. Tasty normalizes related properties into the same style family and resolves that family in a fixed order instead of relying on object insertion order.&lt;/p&gt;

&lt;p&gt;This is an opinionated component model, not a replacement for every use of CSS. Tasty does not model features such as &lt;code&gt;@layer&lt;/code&gt; or &lt;code&gt;!important&lt;/code&gt;, and rules whose intent is genuinely document-wide may be clearer as ordinary CSS. Tasty prioritizes local, explicit ownership; ordinary CSS exposes the full cascade and structural reach. Choosing an execution point does not erase that design tradeoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime capability has to earn its place
&lt;/h2&gt;

&lt;p&gt;Runtime-capable styling is one choice in a wider landscape. Teams can use ordinary CSS, preprocessors, utility or variant vocabularies, static extraction, server generation, browser generation, or combinations of them. Each makes different tradeoffs in cascade control, source visibility, tooling, delivery, composition, and browser work. None is right by category alone.&lt;/p&gt;

&lt;p&gt;Runtime capability earns its place when final styles depend on information unavailable earlier, or when a narrower component API cannot express a legitimate product need. The rule is to generate CSS as soon as the required styles are known and defer only what remains open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;During the build&lt;/strong&gt; when every required style becomes known while generating the application, whether through static analysis or build-time rendering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On the server&lt;/strong&gt; when the decision is request-specific but complete before the HTML is sent or streamed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In the browser&lt;/strong&gt; when values or composition remain open after server rendering, or when making every consumer participate in extraction would cost more than the browser work being removed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Profile products, not categories. The old evidence described real costs in particular implementations and workloads; it did not settle every use of runtime generation. The relevant question is whether the composition and ownership it enables are worth the cost in your product. If they are not, generate the CSS earlier or choose a simpler approach.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>performance</category>
      <category>react</category>
    </item>
    <item>
      <title>OKHST: predictable color generation for user interfaces</title>
      <dc:creator>Andrey Yamanov</dc:creator>
      <pubDate>Wed, 12 Aug 2026 10:27:43 +0000</pubDate>
      <link>https://dev.to/tenphi/okhst-predictable-color-generation-for-user-interfaces-2o12</link>
      <guid>https://dev.to/tenphi/okhst-predictable-color-generation-for-user-interfaces-2o12</guid>
      <description>&lt;p&gt;Wouldn't it be great if one &lt;code&gt;hue&lt;/code&gt; slider could recolor an entire interface, while a &lt;code&gt;saturation&lt;/code&gt; slider controlled how colorful the whole system felt?&lt;/p&gt;

&lt;p&gt;I don't mean a beautiful gradient or a decorative palette. I mean the boring parts of an interface: backgrounds, text, borders, hover states, selected states, disabled states, shadows, dark mode, and high contrast.&lt;/p&gt;

&lt;p&gt;The premise sounds simple: pick a hue and saturation, then derive everything else. I chased that idea through several color spaces before finding the assumption that kept breaking it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;lightness is not contrast.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OKHST is the coordinate system I eventually built around that distinction. A CSS playground later in the article tests the model in the browser, while Glaze turns it into a production system.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://tenphi.me/blog/okhst-predictable-color-generation/" rel="noopener noreferrer"&gt;tenphi.me&lt;/a&gt;, where the previews are live and the hue slider actually works — here they are screenshots.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old Attempt
&lt;/h2&gt;

&lt;p&gt;About seven years ago, I tried to generate UI colors at runtime. Instead of&lt;br&gt;
hand-picking every token, the framework would take a small set of inputs and&lt;br&gt;
derive a complete scheme. &lt;a href="https://www.hsluv.org/" rel="noopener noreferrer"&gt;HSLuv&lt;/a&gt; made the experiment&lt;br&gt;
possible.&lt;/p&gt;

&lt;p&gt;HSLuv gave me something regular HSL did not: a perceptually useful lightness&lt;br&gt;
axis. In regular HSL, &lt;code&gt;50%&lt;/code&gt; lightness can mean wildly different things for&lt;br&gt;
yellow, blue, red, and green. HSLuv made "make this lighter" and "make this&lt;br&gt;
darker" much closer to operations a UI system could rely on.&lt;/p&gt;

&lt;p&gt;I spent months tuning formulas around it. Eventually, tiny declarations could&lt;br&gt;
color different parts of an interface while the framework kept the palette&lt;br&gt;
consistent. The result was good enough for production, and it was one of the&lt;br&gt;
most fun things I had built at the time.&lt;/p&gt;

&lt;p&gt;The old demo is still online: &lt;a href="https://theme.tenphi.me" rel="noopener noreferrer"&gt;theme.tenphi.me&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftfxyuc364q6okw95nykt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftfxyuc364q6okw95nykt.png" alt="CSS Color Theme Generator at theme.tenphi.me, showing hue and saturation sliders with a live preview of buttons, cards, and badges." width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But the system was coupled to one design system, embedded in our framework, and&lt;br&gt;
tuned for one product. HSLuv was also still immature, so several inconsistencies&lt;br&gt;
needed local workarounds.&lt;/p&gt;

&lt;p&gt;The experiment convinced me that UI colors could be computed, but it never&lt;br&gt;
became a reusable tool. I kept using HSLuv, stopped shipping generated&lt;br&gt;
palettes, and waited for a reason to revisit the idea.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Web Caught Up
&lt;/h2&gt;

&lt;p&gt;That reason arrived with OKLCH in CSS. It gives the web a modern perceptual&lt;br&gt;
color space and makes many color operations more sensible than HSL ever could.&lt;br&gt;
But direct palette generation remains difficult because &lt;code&gt;C&lt;/code&gt; is absolute chroma,&lt;br&gt;
not the relative "saturation" control people expect when authoring UI colors.&lt;br&gt;
The maximum possible chroma changes with hue and lightness, so keeping chroma&lt;br&gt;
fixed while changing lightness can push some colors outside sRGB and cause&lt;br&gt;
clipping.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Same chroma, different lightness.
   Some hue/lightness combinations may clip in sRGB. */&lt;/span&gt;
&lt;span class="nc"&gt;.step-1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;35%&lt;/span&gt; &lt;span class="m"&gt;0.18&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.step-2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;65%&lt;/span&gt; &lt;span class="m"&gt;0.18&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.step-3&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;85%&lt;/span&gt; &lt;span class="m"&gt;0.18&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Manual palettes can work around this: pick each value by eye and adjust the&lt;br&gt;
outliers. Dynamic themes cannot do that so easily. When a background, text&lt;br&gt;
color, border, shadow, and hover state all derive from one source color,&lt;br&gt;
clipping silently changes the relationship between them. A generated ramp can&lt;br&gt;
look fine in the middle and fall apart near the edges.&lt;/p&gt;

&lt;p&gt;Modern relative colors make the approach especially tempting:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;65%&lt;/span&gt; &lt;span class="m"&gt;0.18&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="m"&gt;25%&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="m"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The relationship is elegant, but the same absolute chroma follows the&lt;br&gt;
background into each new lightness value. A generated palette can therefore&lt;br&gt;
drift or clip. What I needed was a saturation axis relative to the maximum&lt;br&gt;
chroma available at each hue and lightness.&lt;/p&gt;
&lt;h2&gt;
  
  
  OKHSL Gets Saturation Right
&lt;/h2&gt;

&lt;p&gt;OKHSL provides that missing axis. It has the familiar shape of hue, saturation,&lt;br&gt;
and lightness, but builds it on OKLab/OKLCH math. Its saturation is normalized&lt;br&gt;
against what is possible for the current hue and lightness.&lt;/p&gt;

&lt;p&gt;Near white and black, where highly saturated colors cannot physically exist in&lt;br&gt;
sRGB, OKHSL desaturates gracefully instead of clipping unpredictably.&lt;/p&gt;

&lt;p&gt;This graceful behavior is what a generated UI palette needs.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Illustrative pseudo-code, not a real library call.&lt;/span&gt;
&lt;span class="c1"&gt;// Same saturation, different lightness — OKHSL keeps it in-gamut.&lt;/span&gt;
&lt;span class="nf"&gt;okhsl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;l&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;okhsl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;l&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;okhsl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;l&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In OKHSL, &lt;code&gt;70%&lt;/code&gt; saturation means something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use 70% of the chroma available for this hue and lightness.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is much better for generated UI colors than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use this absolute chroma value and hope it still fits.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OKHSL is limited to sRGB, which may sound disappointing in a world of wide-gamut&lt;br&gt;
displays. For core UI, I often find that constraint useful: repeated text,&lt;br&gt;
icons, and borders benefit from a conservative, in-gamut default. Wide-gamut&lt;br&gt;
color still has a natural place in illustrations, decorations, and brand moments.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Bug in My Mental Model
&lt;/h2&gt;

&lt;p&gt;OKHSL solved the gamut problem, but not the relationship problem. My palettes&lt;br&gt;
still felt wrong in places because I was treating a color's lightness as if it&lt;br&gt;
described its contrast against another color.&lt;/p&gt;

&lt;p&gt;Dark mode exposed the mismatch first. Inverting lightness produced a coherent&lt;br&gt;
palette, but the contrast felt weaker than expected, so I added a separate&lt;br&gt;
transformation curve. Then very dark colors in a light scheme also moved&lt;br&gt;
differently from what I expected. The curve had only hidden the underlying&lt;br&gt;
mistake.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Nice idea, weak invariant:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;light&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;l&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;light&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Perceptual lightness describes how light or dark an individual color appears.&lt;br&gt;
UI contrast describes the relationship between two colors, and equal lightness&lt;br&gt;
changes do not produce equal contrast changes across the range.&lt;/p&gt;

&lt;p&gt;A simple way to see it is to compare a tiny step near black with the same tiny&lt;br&gt;
step near white.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Near black: l = 0, 1, 2, 3, 4
Near white: l = 96, 97, 98, 99, 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The numeric movement is the same. The contrast between adjacent steps is not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F777c3cvubao78koe780h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F777c3cvubao78koe780h.png" alt="Two rows of five swatches. On black, the steps from lightness 0 to 4 are almost indistinguishable; on white, the steps from 96 to 100 separate clearly." width="800" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Near black, two neighboring steps barely separate. Near white, the same gap&lt;br&gt;
reads as a clear jump.&lt;/p&gt;

&lt;p&gt;You can also measure the failure across the whole range. Here is WCAG contrast&lt;br&gt;
for a neutral ramp with equal lightness steps of &lt;code&gt;+20&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lightness&lt;/th&gt;
&lt;th&gt;vs black&lt;/th&gt;
&lt;th&gt;vs white&lt;/th&gt;
&lt;th&gt;+20 step ×&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;1.55&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;1.55&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;2.09&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;3.23&lt;/td&gt;
&lt;td&gt;6.50&lt;/td&gt;
&lt;td&gt;2.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;6.64&lt;/td&gt;
&lt;td&gt;3.16&lt;/td&gt;
&lt;td&gt;1.86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;1.70&lt;/td&gt;
&lt;td&gt;1.70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The numeric steps are identical, but the contrast multiplier between adjacent&lt;br&gt;
steps drifts from 1.55× up to 2.09× and back down to 1.70×. This variation makes&lt;br&gt;
lightness a poor authoring axis for things like borders, hover states, shadows,&lt;br&gt;
and dark-mode inversion.&lt;/p&gt;

&lt;p&gt;I needed an authoring axis where a numeric step described a stable change in UI&lt;br&gt;
contrast. That axis became &lt;strong&gt;tone&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  OKHST
&lt;/h2&gt;

&lt;p&gt;Tone is a &lt;code&gt;0-100&lt;/code&gt; authoring axis designed so that equal differences produce&lt;br&gt;
equal contrast multipliers along the neutral axis. A &lt;code&gt;+20&lt;/code&gt; step should describe&lt;br&gt;
the same relative change whether it starts near black, in the middle, or near&lt;br&gt;
white.&lt;/p&gt;

&lt;p&gt;OKHST keeps OKHSL's hue and saturation and replaces lightness with that tone&lt;br&gt;
axis. Under the hood, tone is a monotonic remap of OKHSL lightness: a given tone&lt;br&gt;
always maps to the same lightness at every hue and saturation. Every OKHST color&lt;br&gt;
is therefore a valid OKHSL color and inherits its gamut behavior and&lt;br&gt;
reversibility.&lt;/p&gt;

&lt;p&gt;The remap uses a normalized logarithm of luminance, offset by the same &lt;code&gt;0.05&lt;/code&gt;&lt;br&gt;
used in the WCAG contrast formula:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tone(Y) = 100 * (ln(Y + 0.05) - ln(0.05)) / (ln(1.05) - ln(0.05))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The shared offset gives tone its useful behavior. WCAG contrast is&lt;br&gt;
&lt;code&gt;(Y_hi + 0.05) / (Y_lo + 0.05)&lt;/code&gt;, so for neutral colors a fixed tone difference&lt;br&gt;
corresponds to a fixed contrast ratio, no matter where on the scale it sits.&lt;/p&gt;

&lt;p&gt;Here is the same measurement as before, but with equal tone steps instead of&lt;br&gt;
equal lightness steps:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tone&lt;/th&gt;
&lt;th&gt;vs black&lt;/th&gt;
&lt;th&gt;vs white&lt;/th&gt;
&lt;th&gt;+20 step ×&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;1.84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;1.84&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;1.84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;3.38&lt;/td&gt;
&lt;td&gt;6.21&lt;/td&gt;
&lt;td&gt;1.84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;6.21&lt;/td&gt;
&lt;td&gt;3.38&lt;/td&gt;
&lt;td&gt;1.84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;1.84&lt;/td&gt;
&lt;td&gt;1.84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each &lt;code&gt;+20&lt;/code&gt; tone step multiplies contrast by exactly the same factor — 1.84×.&lt;br&gt;
Plain lightness does not provide that property.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff0oxx1vxqedncwb2u80x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff0oxx1vxqedncwb2u80x.png" alt="Neutral tone ramp from t=0 to t=100 with ×1.84 labels between every pair of swatches and WCAG readings against black and white below each one." width="800" height="199"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tone lets states be authored as deltas instead of independent colors:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Illustrative pseudo-code, not a real library call.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;primaryTone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hoverDelta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;primary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;okhst&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;primaryTone&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hover&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;okhst&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;primaryTone&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;hoverDelta&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The same pattern can describe tints, shades, borders, and shadows.&lt;/p&gt;

&lt;p&gt;Tone provides a reliable starting relationship, not an accessibility guarantee.&lt;br&gt;
The fixed relationship is exact for neutral colors; once saturation enters,&lt;br&gt;
rendered sRGB luminance can drift from that neutral prediction. A required&lt;br&gt;
contrast floor still has to be checked with WCAG or APCA against the final&lt;br&gt;
rendered color pair.&lt;/p&gt;

&lt;p&gt;Even with that boundary, a methodology can say "move tone by this much" and&lt;br&gt;
produce a consistent starting point instead of searching for every color&lt;br&gt;
independently.&lt;/p&gt;

&lt;p&gt;OKHST is not a universal color science breakthrough. It is a practical&lt;br&gt;
coordinate system for generating UI palettes.&lt;/p&gt;

&lt;p&gt;The full specification is here: &lt;a href="https://github.com/tenphi/okhst" rel="noopener noreferrer"&gt;github.com/tenphi/okhst&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Scheme Mapping
&lt;/h2&gt;

&lt;p&gt;OKHST defines a color coordinate:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hue, saturation, tone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A coordinate alone does not define a theme. The interface adds a separate&lt;br&gt;
scheme layer that decides how authored tones behave in light, dark, and&lt;br&gt;
high-contrast modes. That layer has three parts: tone windows, tone deltas, and&lt;br&gt;
local tone flips.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;tone window&lt;/strong&gt; maps the authored &lt;code&gt;0-100&lt;/code&gt; range into the endpoints a scheme&lt;br&gt;
should actually use. For a light scheme, I might use &lt;code&gt;10-100&lt;/code&gt;: pure white&lt;br&gt;
surfaces remain available, while text avoids unnecessarily harsh black. A dark&lt;br&gt;
scheme can invert tone and map it into &lt;code&gt;15-95&lt;/code&gt;, avoiding both black surfaces and&lt;br&gt;
pure white text. High contrast usually opens the window to the full &lt;code&gt;0-100&lt;/code&gt;&lt;br&gt;
range.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;tone delta&lt;/strong&gt; defines how far a color sits from its parent: a border might be&lt;br&gt;
&lt;code&gt;14&lt;/code&gt; tones away from its surface, for example. Opening the window preserves&lt;br&gt;
that distance, so a subtle border remains subtle. High contrast can widen the&lt;br&gt;
deltas to pull borders, muted text, and accents further from the colors beneath&lt;br&gt;
them.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;local tone flip&lt;/strong&gt; lets one region reverse direction without changing the&lt;br&gt;
whole scheme. The surrounding interface may follow dark mode while an accent&lt;br&gt;
region keeps its original direction.&lt;/p&gt;

&lt;p&gt;Together, these parameters let one authored palette serve light, dark, and both&lt;br&gt;
high-contrast variants instead of becoming four separately maintained&lt;br&gt;
palettes. They are theme logic layered on top of OKHST, not part of the color&lt;br&gt;
space itself.&lt;/p&gt;

&lt;p&gt;With the two layers separated, the next question is whether CSS can preserve&lt;br&gt;
their relationships at runtime.&lt;/p&gt;
&lt;h2&gt;
  
  
  Can This Run in CSS?
&lt;/h2&gt;

&lt;p&gt;The playground keeps the OKHST primitive and its scheme logic live instead of&lt;br&gt;
flattening them into static values. Modern Chromium-based browsers are starting&lt;br&gt;
to support CSS &lt;code&gt;@function&lt;/code&gt;, which allows custom functions with real&lt;br&gt;
calculations. This new primitive made an unreasonable experiment possible:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can OKHSL and OKHST conversion run directly in CSS?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer is yes. The functions port OKHSL's gamut-normalized saturation and&lt;br&gt;
emit standard &lt;code&gt;oklch()&lt;/code&gt; colors without a JavaScript runtime.&lt;/p&gt;

&lt;p&gt;I built a playground here: &lt;a href="https://tenphi.github.io/okhst/" rel="noopener noreferrer"&gt;tenphi.github.io/okhst&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fonat33lnfvao23nzrwwe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fonat33lnfvao23nzrwwe.png" alt="OKHST CSS playground with hue, saturation, steps, and tone window controls, showing an 11-step blue palette from tone 0 to 100." width="799" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The implementation exposes two functions:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Raw OKHST primitive: direct hue/saturation/tone conversion. */&lt;/span&gt;
&lt;span class="nt"&gt;--raw-color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;--okhst&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;var&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;--seed-h&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="nt"&gt;var&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;--seed-s&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="err"&gt;62&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c"&gt;/* UI helper: maps tone into the active scheme before resolving OKHST. */&lt;/span&gt;
&lt;span class="nt"&gt;--ui-color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;--ghst&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;var&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;--seed-h&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="nt"&gt;var&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;--seed-s&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="err"&gt;62&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;--okhst()&lt;/code&gt; is the color-space primitive. The scheme-aware &lt;code&gt;--ghst()&lt;/code&gt; helper&lt;br&gt;
applies the current tone window and optional flip before calling it.&lt;/p&gt;

&lt;p&gt;The playground UI uses the same system as its swatches. Changing hue,&lt;br&gt;
saturation, scheme, or tone window also changes the interface around the ramp.&lt;br&gt;
The two sliders from the opening are now working: hue chooses the color family,&lt;br&gt;
saturation controls its intensity, and the UI follows.&lt;/p&gt;

&lt;p&gt;The surrounding interface uses the same seed as the swatches. Each color is&lt;br&gt;
authored as a tone delta from the surface:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--seed-h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--seed-s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;70&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c"&gt;/* tone deltas: how far each color steps away from the surface */&lt;/span&gt;
  &lt;span class="py"&gt;--d-elev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--d-border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--d-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;94&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--d-muted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;62&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--d-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="py"&gt;--c-surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--ghst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--seed-h&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--c-elev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--ghst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--seed-h&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--d-elev&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="py"&gt;--c-border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--ghst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--seed-h&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--d-border&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="py"&gt;--c-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--ghst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--seed-h&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--d-text&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="py"&gt;--c-muted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--ghst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--seed-h&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--d-muted&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="py"&gt;--c-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--ghst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--seed-h&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--seed-s&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--d-accent&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Saturation and tone both use the &lt;code&gt;0-100&lt;/code&gt; percent form here. Hue uses degrees,&lt;br&gt;
and low saturations such as &lt;code&gt;4&lt;/code&gt; are nearly neutral.&lt;/p&gt;

&lt;p&gt;Scheme behavior reduces to variables:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--tone-lo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--tone-hi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--tone-flip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="nc"&gt;.dark&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--tone-lo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--tone-hi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;95&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--tone-flip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="nc"&gt;.high-contrast&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* --tone-flip is inherited from the active light/dark scheme. */&lt;/span&gt;
  &lt;span class="py"&gt;--tone-lo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--tone-hi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c"&gt;/* wider steps: relationships pull apart, not just the extremes */&lt;/span&gt;
  &lt;span class="py"&gt;--d-elev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--d-border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--d-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--d-muted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;72&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--d-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.accent-zone&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--tone-flip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Because the interface colors derive from deltas, these overrides implement high&lt;br&gt;
contrast for light and dark at once while keeping tone inversion separate.&lt;/p&gt;

&lt;p&gt;The swatches define their own local rules:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.swatch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--tone-flip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* palette itself does not invert in dark mode */&lt;/span&gt;
  &lt;span class="py"&gt;--t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--swatch-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--ghst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--seed-h&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--seed-s&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--t&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--swatch-bg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--ghst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--seed-h&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--seed-s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--t&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Together, the rules keep every color connected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the swatch tone comes from its index&lt;/li&gt;
&lt;li&gt;the text tone is inverted from the background tone&lt;/li&gt;
&lt;li&gt;the text saturation is reduced relative to the source saturation&lt;/li&gt;
&lt;li&gt;the palette itself opts out of dark inversion&lt;/li&gt;
&lt;li&gt;the surrounding UI still follows the active scheme&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rendered with those rules (precomputed, since CSS &lt;code&gt;@function&lt;/code&gt; is not widely&lt;br&gt;
shipped yet), the ramp looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9q58pxgqyd97amkuuqyk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9q58pxgqyd97amkuuqyk.png" alt="Six blue swatches from t=0 to t=100, each labeled with its tone and using inverted, desaturated text derived from the same seed." width="800" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The playground reads final colors with &lt;code&gt;getComputedStyle&lt;/code&gt;, but CSS performs the calculation. The first version was slow enough that dragging the hue slider visibly lagged. After I showed it to &lt;a href="https://kizu.dev/" rel="noopener noreferrer"&gt;Roman Komarov&lt;/a&gt;, his proof-of-concept explained why: a function-local is re-substituted at every &lt;code&gt;var()&lt;/code&gt; that reads it, so one call to the cusp solver expanded its branch gates hundreds of times. Moving those sub-expressions into their own &lt;code&gt;@function&lt;/code&gt;s collapses the expansion, because a function call resolves to a single number at the call boundary.&lt;/p&gt;

&lt;p&gt;Three factorings of the same math are now published side by side, and the&lt;br&gt;
&lt;strong&gt;Measure&lt;/strong&gt; button in the playground times a style recalc on whichever one is&lt;br&gt;
open:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;ms/recalc&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;naive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;282&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sub-functions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;optimized&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Numbers from an M1 Max, 51-step ramp, dark scheme. &lt;code&gt;naive&lt;/code&gt; keeps every&lt;br&gt;
intermediate as a function-local, &lt;code&gt;sub-functions&lt;/code&gt; applies Roman’s factoring&lt;br&gt;
unchanged, and &lt;code&gt;optimized&lt;/code&gt; adds a cache on top: the hue-only cusp setup is&lt;br&gt;
hoisted to &lt;code&gt;:root&lt;/code&gt; so it resolves once per hue instead of once per swatch, and&lt;br&gt;
the OKHST and OKHSL paths are split so each swatch evaluates only the transfer&lt;br&gt;
it needs.&lt;/p&gt;

&lt;p&gt;Runtime cost was no longer the main obstacle; browser support was. CSS&lt;br&gt;
&lt;code&gt;@function&lt;/code&gt; is Chromium-only today (Chrome/Edge 139+), while Firefox and Safari&lt;br&gt;
have not shipped it, so broad production use is likely still years away.&lt;/p&gt;

&lt;p&gt;The experiment still proves that CSS can preserve palette rules instead of&lt;br&gt;
flattening them into static values. The platform is expressive enough for the&lt;br&gt;
model, just not widely supported enough to depend on yet.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Hue Slider Loophole
&lt;/h2&gt;

&lt;p&gt;Full OKHST conversion in CSS still has to wait, but my blog at &lt;a href="https://tenphi.me" rel="noopener noreferrer"&gt;tenphi.me&lt;/a&gt; already ships one narrow slice of the idea across modern browsers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgjmq9mgo8kj0l1ngqffa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgjmq9mgo8kj0l1ngqffa.png" alt="Site theme popover with a hue slider, system/light/dark theme controls, and high-contrast options." width="618" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The palette icon in its top bar opens a hue slider. Dragging it rotates the&lt;br&gt;
site's text, surfaces, borders, and accents without &lt;code&gt;@function&lt;/code&gt; or JavaScript&lt;br&gt;
color math. Underneath is the same pattern I called fragile earlier:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;--text-color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;oklch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;201&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;0014&lt;/span&gt; &lt;span class="nt"&gt;var&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;--primary-hue&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The apparent contradiction disappears because every rotating color stays&lt;br&gt;
inside what I call &lt;strong&gt;pastel space&lt;/strong&gt;: chroma remains below the level that fits&lt;br&gt;
inside sRGB for every hue at that lightness. Within that boundary, hue can&lt;br&gt;
rotate without clipping or overshooting.&lt;/p&gt;

&lt;p&gt;The shortcut has three costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Saturation is capped.&lt;/strong&gt; Pastel space excludes vivid colors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only hue remains dynamic.&lt;/strong&gt; Tone windows, state changes, and deltas must be
baked into static values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each pastel limit must be precomputed.&lt;/strong&gt; Otherwise, every color needs a CSS
function or an impractically large inline calculation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site performs that work at build time, leaving only hue as a custom&lt;br&gt;
property. The loophole is useful but narrow: one dynamic axis, muted colors,&lt;br&gt;
and precomputation behind the scenes. A production system has to provide that&lt;br&gt;
precomputation.&lt;/p&gt;
&lt;h2&gt;
  
  
  From Experiment to Production
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/tenphi" rel="noopener noreferrer"&gt;
        tenphi
      &lt;/a&gt; / &lt;a href="https://github.com/tenphi/glaze" rel="noopener noreferrer"&gt;
        glaze
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      OKHST-based accessible color palette generator with support for dark and high-contrast themes.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/tenphi/glaze/assets/glaze.svg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Ftenphi%2Fglaze%2FHEAD%2Fassets%2Fglaze.svg" width="128" height="128" alt="Glaze logo"&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Glaze&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;
  OKHST-based color theme generator with WCAG and APCA contrast solving
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://www.npmjs.com/package/@tenphi/glaze" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/66e79bb2efe4b31e5b1354737ffb69ce1ff2fda93b52a0ad5e523676154ae350/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f4074656e7068692f676c617a652e737667" alt="npm version"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/tenphi/glaze/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/tenphi/glaze/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/tenphi/glaze/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/dc455fc739457e8bd2edf8c33ae2bf9fb984a828c1291cded26ba477bd8561d8/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f6c2f4074656e7068692f676c617a652e737667" alt="license"&gt;&lt;/a&gt;
&lt;/p&gt;




&lt;p&gt;Glaze generates &lt;strong&gt;light&lt;/strong&gt;, &lt;strong&gt;dark&lt;/strong&gt;, and &lt;strong&gt;high-contrast&lt;/strong&gt; color schemes from a
single hue/saturation seed. Relationships stay explicit: colors either define
an absolute tone or depend on a named base through a tone delta and an optional
contrast floor.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OKHST color space&lt;/strong&gt; — OKHSL with a contrast-shaped &lt;strong&gt;tone&lt;/strong&gt; axis. Equal
steps give equal WCAG contrast for neutrals and a useful approximation for
chromatic colors. See &lt;a href="https://github.com/tenphi/glaze/docs/okhst.md" rel="noopener noreferrer"&gt;OKHST in Glaze&lt;/a&gt; and the
&lt;a href="https://github.com/tenphi/okhst" rel="noopener noreferrer"&gt;full OKHST specification&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WCAG 2 + APCA contrast solving&lt;/strong&gt; — automatic tone adjustment to meet a WCAG ratio or APCA Lc floor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unified dark mode&lt;/strong&gt; — one tone space for light, dark, and high-contrast; dark is a single &lt;code&gt;100 − t&lt;/code&gt; inversion, no fitted curve&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mix colors&lt;/strong&gt; — blend two colors with OKHSL or sRGB interpolation, opaque or transparent, with optional contrast solving&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow colors&lt;/strong&gt; — OKHSL-native…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/tenphi/glaze" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;a href="https://github.com/tenphi/glaze" rel="noopener noreferrer"&gt;Glaze&lt;/a&gt; provides that precomputation layer. It applies OKHST and resolves the palette rules that browsers cannot yet evaluate broadly at runtime. It supports dependent colors, solves WCAG and APCA contrast, handles light, dark, and high-contrast schemes, and exports standard design-token formats.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tenphi.me" rel="noopener noreferrer"&gt;tenphi.me&lt;/a&gt; is a live demo. Glaze generates its hue-rotating UI palette and its syntax-highlighting palette. The dark and high-contrast modes behind the same palette icon are scheme remaps computed at build time: parameter changes rather than hand-authored alternate palettes. &lt;a href="https://tasty.style" rel="noopener noreferrer"&gt;tasty.style&lt;/a&gt; uses the same approach for its theme variants.&lt;/p&gt;

&lt;p&gt;The model also scales beyond a single site. &lt;a href="https://github.com/cube-js/cube-ui-kit" rel="noopener noreferrer"&gt;Cube UI Kit&lt;/a&gt;, an open-source React library with more than 100 components that powers &lt;a href="https://cube.dev/" rel="noopener noreferrer"&gt;Cube&lt;/a&gt; products, uses one set of Glaze palette rules across its component library. Glaze deserves its own article because it provides a palette-building methodology, not just a color-space implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The goal is not to make UIs more colorful. It is to make the boring parts of color reliable.&lt;/p&gt;

&lt;p&gt;Normalized saturation keeps hue and lightness changes inside a usable gamut. Tone turns movement along the lightness axis into regular contrast steps. Scheme mapping builds those properties into reusable rules for states, dark mode, and high contrast. Glaze precomputes those rules for production today.&lt;/p&gt;

&lt;p&gt;OKHST is not perfect or universal, but it gives UI color generation a more useful set of coordinates. The question from the opening is no longer hypothetical: open &lt;a href="https://tenphi.me" rel="noopener noreferrer"&gt;tenphi.me&lt;/a&gt;, click the palette icon in the top bar, drag the &lt;code&gt;hue&lt;/code&gt; slider, and watch the boring parts follow.&lt;/p&gt;

</description>
      <category>color</category>
      <category>css</category>
      <category>uidesign</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why I spent years trying to make CSS states predictable</title>
      <dc:creator>Andrey Yamanov</dc:creator>
      <pubDate>Thu, 23 Apr 2026 12:11:25 +0000</pubDate>
      <link>https://dev.to/tenphi/why-i-spent-years-trying-to-make-css-states-predictable-a9c</link>
      <guid>https://dev.to/tenphi/why-i-spent-years-trying-to-make-css-states-predictable-a9c</guid>
      <description>&lt;p&gt;Have you ever changed the order of two CSS rules and broken a component without changing the logic?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.btn&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;dodgerblue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.btn&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both selectors have specificity &lt;code&gt;(0, 1, 1)&lt;/code&gt;. When a button is both hovered and disabled, the browser falls back to source order. If the &lt;code&gt;:hover&lt;/code&gt; rule comes last, the disabled button turns blue. If the &lt;code&gt;[disabled]&lt;/code&gt; rule comes last, it stays gray.&lt;/p&gt;

&lt;p&gt;That looks like a small edge case, but it points to a bigger problem: component state in CSS often works by overlap.&lt;/p&gt;

&lt;p&gt;With only one or two states, that overlap feels manageable. Add &lt;code&gt;:hover&lt;/code&gt;, &lt;code&gt;:active&lt;/code&gt;, &lt;code&gt;disabled&lt;/code&gt;, dark mode, breakpoints, data attributes, container queries, and overrides, and you are no longer just writing styles. You are maintaining a state-resolution system in your head.&lt;/p&gt;

&lt;p&gt;I kept running into this while building component systems: real buttons, inputs, panels, dropdowns, and design-system primitives. Writing the first version was rarely the difficult part. Extending it later without reopening the entire state-resolution problem was.&lt;/p&gt;

&lt;p&gt;The result was more than the occasional source-order bug. As requirements accumulated, even small customizations became harder to make safely.&lt;/p&gt;

&lt;p&gt;At some point I stopped asking, "How do I write this selector?" and started asking a better question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if component state could be expressed declaratively, while the compiler handled the selector logic needed to make it deterministic?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question eventually became &lt;a href="https://tasty.style" rel="noopener noreferrer"&gt;Tasty&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea in one minute
&lt;/h2&gt;

&lt;p&gt;Instead of writing selectors that compete through cascade and specificity, I wanted to describe a property's possible states as a map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tasty&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tenphi/tasty&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tasty&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:hover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#primary-hover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#primary-pressed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[disabled]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#surface&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read in order of priority, this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if the button is disabled, use &lt;code&gt;#surface&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;otherwise, if it is active, use &lt;code&gt;#primary-pressed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;otherwise, if it is hovered, use &lt;code&gt;#primary-hover&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;otherwise, use &lt;code&gt;#primary&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is what happens next.&lt;/p&gt;

&lt;p&gt;Tasty compiles that state map into selectors that cannot overlap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* [disabled] wins outright */&lt;/span&gt;
&lt;span class="nc"&gt;.t0&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--surface-color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;/* :active is excluded when disabled */&lt;/span&gt;
&lt;span class="nc"&gt;.t0&lt;/span&gt;&lt;span class="nd"&gt;:active:not&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;&lt;span class="nt"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--primary-pressed-color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;/* :hover is excluded when :active or disabled */&lt;/span&gt;
&lt;span class="nc"&gt;.t0&lt;/span&gt;&lt;span class="nd"&gt;:hover:not&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:active&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nd"&gt;:not&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;&lt;span class="nt"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--primary-hover-color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;/* default is excluded when anything above matches */&lt;/span&gt;
&lt;span class="nc"&gt;.t0&lt;/span&gt;&lt;span class="nd"&gt;:not&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nd"&gt;:not&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:active&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nd"&gt;:not&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;&lt;span class="nt"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--primary-color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No selector conflict remains for specificity or source order to settle. Only one branch can match at a time.&lt;/p&gt;

&lt;p&gt;The real payoff comes later: changing or extending the map is much easier than reconstructing the equivalent selector relationships by hand.&lt;/p&gt;

&lt;p&gt;That is the central idea:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the author defines the priority, the generated selectors should make that priority unambiguous.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than the button example suggests
&lt;/h2&gt;

&lt;p&gt;A hovered disabled button is simply the easiest way to see the problem. The real difficulty begins when conditions intersect in less obvious ways.&lt;/p&gt;

&lt;p&gt;Maybe dark mode can come from a root attribute, or from &lt;code&gt;prefers-color-scheme&lt;/code&gt;, or from both. Maybe spacing changes inside a narrow container, but only on tablet widths. Maybe a destructive variant behaves differently on hover but not when loading. Maybe a parent theme toggles a child override.&lt;/p&gt;

&lt;p&gt;Each rule is understandable in isolation. The difficulty lies in keeping their relationships predictable. Small edits can change which branches overlap. A harmless refactor can turn into a source-order bug. Extending an existing component can mean reopening selector logic you thought was already settled.&lt;/p&gt;

&lt;p&gt;I wanted a model where adding a new state did not mean mentally re-deriving the whole selector matrix.&lt;/p&gt;

&lt;h2&gt;
  
  
  A slightly bigger example
&lt;/h2&gt;

&lt;p&gt;Consider a reusable action card. It can appear in the main content or a narrow sidebar, adapt its layout to the viewport, and represent either a regular or destructive action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ActionCard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tasty&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;column&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@media(w &amp;gt;= 768px)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;row&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#surface&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@root(schema=dark)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#surface-dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme=danger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#danger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme=danger &amp;amp; :hover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#danger-hover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4x&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@(sidebar, w &amp;lt; 300px)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2x&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The card stacks its contents by default and arranges them in a row on wider screens. Its background follows the root color scheme unless the danger theme applies, with a separate danger hover state. Inside a narrow sidebar container, its padding shrinks. The author declares these decisions next to the properties they affect; the compiler handles their selector relationships and priority.&lt;/p&gt;

&lt;p&gt;The object-shaped representation has another practical consequence. All styles for a particular element and its sub-elements can live in a single style object. Merging a second style object into the first naturally extends the entire definition—including its state maps—without sending the author back to scattered selector overrides.&lt;/p&gt;

&lt;p&gt;That makes extension a form of data composition. Authors combine style definitions; the compiler resolves the resulting states. This matters because a component system rarely stays as it was first written.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it took so long
&lt;/h2&gt;

&lt;p&gt;The core idea is simple. Turning it into a system I could trust across a design system took several years and hundreds of iterations.&lt;/p&gt;

&lt;p&gt;Producing one clever selector was not the challenge. The system had to remain coherent when all of these appeared together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pseudo-classes like &lt;code&gt;:hover&lt;/code&gt; and &lt;code&gt;:active&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;attributes, boolean modifiers, and value-based modifiers&lt;/li&gt;
&lt;li&gt;root-level state&lt;/li&gt;
&lt;li&gt;media queries&lt;/li&gt;
&lt;li&gt;container queries&lt;/li&gt;
&lt;li&gt;nested and compound selectors&lt;/li&gt;
&lt;li&gt;merging style objects across elements and sub-elements&lt;/li&gt;
&lt;li&gt;typed APIs on top of the styling model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Supporting each feature was only part of the work. Every addition also had to compose with everything already there. Media queries had to coexist with modifiers. Root state had to coexist with pseudo-classes. Extending a style object had to preserve its declared priorities rather than quietly reshuffle them.&lt;/p&gt;

&lt;p&gt;As the model grew, I repeatedly had to test whether the original idea still held up. Sometimes it did. Sometimes it very much did not. There were stretches where I broke the DSL, reconsidered how states should be represented internally, and rebuilt large parts of the compiler to preserve the same promise.&lt;/p&gt;

&lt;p&gt;The technical work involved parsing, normalization, selector generation, caching, extension rules, and making the output fast enough to be practical. The conceptual work was deciding what Tasty actually was.&lt;/p&gt;

&lt;p&gt;Was it a nicer CSS object format? An atomic CSS generator? A design-system language? A compiler for stateful component styles? For a while, Tasty seemed to be all of these at once. The difficult part was finding boundaries that made those roles reinforce one another instead of compete.&lt;/p&gt;

&lt;p&gt;For a long time I honestly did not know whether the idea could scale cleanly enough to justify the effort. It worked in pockets early. Making it dependable across a design system took much longer.&lt;/p&gt;

&lt;p&gt;Tasty has powered &lt;a href="https://github.com/cube-js/cube-ui-kit" rel="noopener noreferrer"&gt;Cube UI Kit&lt;/a&gt; from the beginning. That system now spans 100+ components and powers &lt;a href="https://cube.dev/product/cube-cloud" rel="noopener noreferrer"&gt;Cube Cloud&lt;/a&gt;, a real enterprise product. Early versions were absolutely experimental internally. Production pressure and team feedback exposed where the model held up and where it needed to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I care about most
&lt;/h2&gt;

&lt;p&gt;I do not think mutually exclusive selectors are interesting merely because they are clever. They are interesting because they remove a category of ambiguity that should not be the author's job in the first place.&lt;/p&gt;

&lt;p&gt;When I style a component, I want to describe what it should look like in each meaningful state. I do not want to manually encode the browser's tie-breaker logic every time those states intersect.&lt;/p&gt;

&lt;p&gt;That is the payoff Tasty is chasing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;predictable component behavior&lt;/li&gt;
&lt;li&gt;fewer accidental regressions from source order&lt;/li&gt;
&lt;li&gt;easier extension through composable style objects&lt;/li&gt;
&lt;li&gt;a styling model that becomes more valuable as the design system grows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are styling a small landing page, this is probably too much machinery. Plain CSS is often the right answer.&lt;/p&gt;

&lt;p&gt;If you are building components that need to survive years of iteration, variant growth, theme expansion, and multiple authors, predictability starts compounding in a practical way.&lt;/p&gt;

&lt;p&gt;Tasty also has typed component APIs, sub-elements, SSR integrations, zero-runtime extraction, editor tooling, linting, tokens, recipes, and more. Those features matter, but they are downstream of the principle that motivated the project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Component states should be easy to describe and hard to make ambiguous.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That sentence took years to turn into a tool I was comfortable releasing.&lt;/p&gt;

&lt;h2&gt;
  
  
  If this resonates
&lt;/h2&gt;

&lt;p&gt;You can try Tasty in the browser with the &lt;a href="https://tasty.style/playground" rel="noopener noreferrer"&gt;playground&lt;/a&gt;, or read the &lt;a href="https://tasty.style/docs" rel="noopener noreferrer"&gt;docs&lt;/a&gt; if you want the full language and feature set.&lt;/p&gt;

&lt;p&gt;If you try it, I would especially like to hear where the model clicks, where it feels unfamiliar, and where the documentation skips a mental step. That kind of feedback has shaped the project from the beginning, and it still does. The best place to share it is &lt;a href="https://github.com/tenphi/tasty/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Tasty grew from a problem I kept encountering for years. If that problem feels familiar, I would love to know whether this model helps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links&lt;/strong&gt;: &lt;a href="https://tasty.style" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; | &lt;a href="https://tasty.style/playground" rel="noopener noreferrer"&gt;Playground&lt;/a&gt; | &lt;a href="https://github.com/tenphi/tasty" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://www.npmjs.com/package/@tenphi/tasty" rel="noopener noreferrer"&gt;npm&lt;/a&gt; | &lt;a href="https://telegram.me/tasty_css" rel="noopener noreferrer"&gt;Telegram&lt;/a&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>designsystem</category>
    </item>
  </channel>
</rss>
