<?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: David Foliti</title>
    <description>The latest articles on DEV Community by David Foliti (@marlenesco).</description>
    <link>https://dev.to/marlenesco</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%2F163562%2F80a0475e-b0d6-4b2e-a396-c187549324b4.jpeg</url>
      <title>DEV Community: David Foliti</title>
      <link>https://dev.to/marlenesco</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marlenesco"/>
    <language>en</language>
    <item>
      <title>A palette wasn't enough, so I built a color system generator</title>
      <dc:creator>David Foliti</dc:creator>
      <pubDate>Mon, 17 Aug 2026 16:30:49 +0000</pubDate>
      <link>https://dev.to/marlenesco/a-palette-wasnt-enough-so-i-built-a-color-system-generator-3lk5</link>
      <guid>https://dev.to/marlenesco/a-palette-wasnt-enough-so-i-built-a-color-system-generator-3lk5</guid>
      <description>&lt;p&gt;I wanted a workflow that sounded straightforward: start with a color and turn it into something I could use in a frontend project.&lt;/p&gt;

&lt;p&gt;There are already plenty of good palette generators. I wasn't interested in building another tool that stops after producing eleven attractive swatches. For me, that's where the actual work begins.&lt;/p&gt;

&lt;p&gt;Which shade should drive the main action? What happens to it in dark mode? Which foreground works on top of it? And does the palette still hold together once it reaches buttons, cards, borders, charts and larger surfaces?&lt;/p&gt;

&lt;p&gt;I started exploring those questions in my spare time. That experiment eventually became &lt;a href="https://tintary.app" rel="noopener noreferrer"&gt;Tintary&lt;/a&gt;, a free color-system generator with no account or sign-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  A palette is source material, not the final result
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;50–950&lt;/code&gt; scale is useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;50  100  200  300  400  500  600  700  800  900  950
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But applications rarely think only in terms of &lt;code&gt;purple-600&lt;/code&gt; or &lt;code&gt;blue-200&lt;/code&gt;. They also need roles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;primary
surface
surface-muted
foreground
border
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That distinction became central to Tintary. Raw scales preserve the available color range; semantic roles explain how those colors should be used. A role can resolve differently in light and dark mode without changing the underlying source palette.&lt;/p&gt;

&lt;p&gt;Instead of treating a palette as finished output, Tintary treats it as material for a small, reusable color system.&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%2Fqwsqknd9kb25ottrazyr.jpg" 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%2Fqwsqknd9kb25ottrazyr.jpg" alt="Tintary raw scale and semantic color roles" width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Making eleven shades behave like one family
&lt;/h2&gt;

&lt;p&gt;I underestimated scale generation at first. Moving toward white and black produces eleven colors, but not necessarily eleven useful steps.&lt;/p&gt;

&lt;p&gt;Tintary uses Culori for parsing, conversion, color distance and contrast calculations. The scale recipe follows the public approach used by UIColors: for chromatic scales, it compares the source against Tailwind v3 reference families using CIEDE2000, finds the closest progression, then adapts that progression around the input color.&lt;/p&gt;

&lt;p&gt;The source isn't automatically forced into the &lt;code&gt;500&lt;/code&gt; slot. Tintary places it at the stop with the closest lightness, so an input may become &lt;code&gt;400&lt;/code&gt;, &lt;code&gt;500&lt;/code&gt; or &lt;code&gt;600&lt;/code&gt;. The remaining shades grow around it, and every result is available as HEX, RGB, HSL and OKLCH.&lt;/p&gt;

&lt;p&gt;Large semantic surfaces need different treatment from raw shade stops. Tintary generates those through a separate, bounded OKLCH recipe so light and dark surfaces keep a useful depth order without becoming overwhelmingly chromatic.&lt;/p&gt;

&lt;p&gt;The conversion code wasn't the difficult part. The difficult part was preserving relationships: a very light shade should work as a surface, middle shades should remain useful for actions, and dark shades should still feel connected to the original color.&lt;/p&gt;

&lt;h2&gt;
  
  
  The foreground problem
&lt;/h2&gt;

&lt;p&gt;Foreground selection looked simple until I compared the numbers with what I preferred on screen.&lt;/p&gt;

&lt;p&gt;Take &lt;code&gt;#ED002F&lt;/code&gt; as a background. Using literal black and white for a small example, WCAG contrast works out to roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;black: &lt;code&gt;4.64:1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;white: &lt;code&gt;4.52:1&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Black wins, but only narrowly. Both pass WCAG AA for normal text; neither reaches AAA. Side by side, I sometimes still prefer white on this particular red.&lt;/p&gt;

&lt;p&gt;That led me to keep two ideas separate:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Contrast is measurable. Readability still needs to be inspected in context.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tintary supports three kinds of foreground rule: a Y/luma heuristic, maximum WCAG contrast, and a guarded strategy that keeps the heuristic choice only when it reaches a configured threshold.&lt;/p&gt;

&lt;p&gt;The Check stage then evaluates the resulting foreground/background pairs independently against the active policy, WCAG AA and WCAG AAA. Choosing a foreground and validating its contrast are related operations, but they are not the same operation.&lt;/p&gt;

&lt;p&gt;I didn't want the tool to hide that distinction behind one generic “accessible” badge. Showing the ratio, threshold and affected role makes the trade-off much easier to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Colors need consequences
&lt;/h2&gt;

&lt;p&gt;This is the part of Tintary I use most now.&lt;/p&gt;

&lt;p&gt;A row of swatches can look balanced while hiding practical problems. Once applied to an interface, the same palette may reveal that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the primary button dominates everything around it;&lt;/li&gt;
&lt;li&gt;the muted surface isn't muted enough;&lt;/li&gt;
&lt;li&gt;borders disappear in one mode;&lt;/li&gt;
&lt;li&gt;chart series are difficult to distinguish;&lt;/li&gt;
&lt;li&gt;a gradient that looked good in isolation feels terrible behind content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tintary's Preview stage applies the generated system to interface patterns, component states and more visual compositions. It isn't trying to simulate every possible product. It gives colors enough context to expose weak relationships early.&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%2F1ik72i9y2ru6cwyyj1xi.jpg" 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%2F1ik72i9y2ru6cwyyj1xi.jpg" alt="Palette Lab preview using three source colors" width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tintary.app/colors/e544c5/1b90ee/e1890f/preview/palette-lab" rel="noopener noreferrer"&gt;Open this three-color setup in Palette Lab&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One color model, five export adapters
&lt;/h2&gt;

&lt;p&gt;I wanted Tintary's internal model to remain independent from any CSS framework. Tailwind is used to build the app's interface, but it doesn't define the generated theme.&lt;/p&gt;

&lt;p&gt;Tintary currently exports the same color system in five formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generic CSS custom properties;&lt;/li&gt;
&lt;li&gt;Tailwind CSS v4 &lt;code&gt;@theme&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Material UI theme configuration;&lt;/li&gt;
&lt;li&gt;Bootstrap 5.3 variables;&lt;/li&gt;
&lt;li&gt;Figma / Tokens Studio JSON.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each exporter acts as an adapter. It consumes the same generated theme and formats it for a target; it doesn't regenerate or reinterpret the colors.&lt;/p&gt;

&lt;p&gt;For example, the same scale can become CSS:&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;--color-primary-50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
  &lt;span class="py"&gt;--color-primary-500&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
  &lt;span class="py"&gt;--color-primary-950&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;or Tailwind v4 output:&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="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-primary-50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
  &lt;span class="py"&gt;--color-primary-500&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;
  &lt;span class="py"&gt;--color-primary-950&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;If I stop using Tailwind tomorrow, the color model shouldn't care.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I kept the workspace local
&lt;/h2&gt;

&lt;p&gt;Tintary has no authentication, backend or cloud palette database. Workspace settings and saved themes stay in the browser through &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Source colors live in the URL path, while non-color workspace settings can travel in a versioned URL hash. That means a setup can be bookmarked or shared without creating an account:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tintary.app/colors/e544c5/1b90ee/e1890f/preview/palette-lab" rel="noopener noreferrer"&gt;tintary.app/colors/e544c5/1b90ee/e1890f/preview/palette-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also save named theme versions in the browser, or download a versioned JSON snapshot and import it again later. I wanted portability, but I didn't want registration to become the price of saving three colors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding where to stop
&lt;/h2&gt;

&lt;p&gt;Tintary is intentionally color-only. It doesn't generate typography scales, spacing systems, shadows, radius tokens, motion tokens or complete UI components.&lt;/p&gt;

&lt;p&gt;It also doesn't try to become a full design-system platform. Its workflow has a narrower boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build → Preview → Check → Export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You bring source colors, generate the system, inspect it in context, check foreground and contrast decisions, then export it. That boundary keeps the tool useful without turning it into a large visual editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the engine independent
&lt;/h2&gt;

&lt;p&gt;Tintary is built with Next.js, TypeScript, Tailwind CSS, shadcn/ui, Culori and Zustand. Vitest, Playwright and axe-core cover unit, browser and accessibility checks.&lt;/p&gt;

&lt;p&gt;The architectural flow is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source colors
     ↓
Color engine
     ↓
Raw 50–950 scales
     ↓
Semantic roles
     ↓
Preview / Check
     ↓
Export adapters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Color generation lives in pure functions, separate from Zustand editor state and framework exporters. That choice has made new export targets easier to add and, more importantly, prevents framework-specific assumptions from leaking back into the theme.&lt;/p&gt;

&lt;h2&gt;
  
  
  What building Tintary changed for me
&lt;/h2&gt;

&lt;p&gt;I started with the idea that palette generation was the main problem. It wasn't. The more interesting questions came afterwards: how much should the tool decide, which decisions should remain visible, and where should accessibility checks inform the workflow without pretending to replace design judgment?&lt;/p&gt;

&lt;p&gt;Tintary's answer is to keep one framework-agnostic model, offer explicit foreground policies, and report contrast results without silently rewriting the user's colors.&lt;/p&gt;

&lt;p&gt;That isn't the only valid approach, but it is the boundary that currently makes sense to me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try Tintary
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://tintary.app" rel="noopener noreferrer"&gt;Tintary is free to use&lt;/a&gt;, has no paid tier and doesn't require an account. The codebase is private.&lt;/p&gt;

&lt;p&gt;I built it because I wanted this workflow for my own frontend projects. It's still evolving, and bugs, criticism and suggestions are welcome.&lt;/p&gt;

&lt;p&gt;If you try it, I'd love to know one thing: &lt;strong&gt;is the workflow clear and useful for real frontend work, and where could the experience be improved?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>design</category>
      <category>color</category>
      <category>ui</category>
    </item>
  </channel>
</rss>
