<?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: Andriy Yurchenko</title>
    <description>The latest articles on DEV Community by Andriy Yurchenko (@andyur).</description>
    <link>https://dev.to/andyur</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%2F4243511%2F32195abd-5a01-4ddf-9be5-33aa971af94a.png</url>
      <title>DEV Community: Andriy Yurchenko</title>
      <link>https://dev.to/andyur</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/andyur"/>
    <language>en</language>
    <item>
      <title>Building Ribbonlab, a 3D Animated Background Generator</title>
      <dc:creator>Andriy Yurchenko</dc:creator>
      <pubDate>Fri, 13 Nov 2026 19:32:39 +0000</pubDate>
      <link>https://dev.to/andyur/building-ribbonlab-a-3d-animated-background-generator-2med</link>
      <guid>https://dev.to/andyur/building-ribbonlab-a-3d-animated-background-generator-2med</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://ayurchenko.xyz/writing/ribbonlab" rel="noopener noreferrer"&gt;ayurchenko.xyz&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ribbonlab.ayurchenko.xyz/" rel="noopener noreferrer"&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%2Fsms9580jxvhmky3ltoi0.jpg" alt="The live ribbon in the Ribbonlab editor" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The image above opens the live editor. Ribbonlab is a free browser editor for 3D animated backgrounds. I built it on my own over a few evenings of spare time. Claude Code wrote most of the code. I decided what the tool does, looked at every change in the browser and said what was wrong until it looked right. This piece shows that work on the three parts that took the most of it: the textures, the shape and the settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the work went
&lt;/h2&gt;

&lt;p&gt;Every change went the same way. I opened the editor, looked at the ribbon and wrote what was wrong in one line, usually with a screenshot: one edge of the wave looks buggy, the glitter sits on top and does not move, this texture darkens the ribbon. Claude Code found the cause in the shader or the panel and fixed it. I checked the result in the browser and either moved on or sent the next line. The repository has 243 commits from 8 to 22 October, most of them one change I could see.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq4gerfcus6jt627rr3jx.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq4gerfcus6jt627rr3jx.webp" alt="The Ribbonlab editor: a green and yellow fibre ribbon across a white page, the dark tuning panel on the right with the Aurora palette and five colors, the shortcut bar at the bottom" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The editor on the Aurora preset. The ribbon fills the page and the panel stays to one side.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Textures
&lt;/h2&gt;

&lt;p&gt;One version of the silk read like a muscle with veins. The threads were thick and contrasty, and the light lay across the surface in wavy bundles, pale on dark blue. The fix was a smooth surface with fine, straight threads along the band. Then I tuned the threads by eye and kept the values: Streaks from 0.28 to 0.44, Glow from 1.3 to 0.89, Grain from 3.5 to 3.1. At those values the silk reads as millions of threads.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6vfpuduu9so4911b8a1q.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6vfpuduu9so4911b8a1q.webp" alt="A blue ribbon close up with thick wavy bands of light across it, like muscle fibres" width="800" height="500"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv4s9bh8hj66d8nm4d3wp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv4s9bh8hj66d8nm4d3wp.webp" alt="A blue silk ribbon close up with fine straight threads running along a smooth surface" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Left: the silk that read like a muscle. Right: the silk that shipped.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next I asked for textures as good as the silk, and the answer started with why the silk worked. Its threads follow the band, so every twist bends the pattern with it. Their light depends on how the surface turns, so the pattern moves when the ribbon moves. Leopard, zebra and polygonal were prints laid on top, and they stayed flat. The textures added after that follow both rules: moir√© from two layers of threads drifting in and out of step, guilloch√© lines that swell in shadow and thin in light, chameleon paint whose hue turns with the viewing angle, mercury that mirrors a studio with softboxes sliding over it, and a twill weave. Snake did not look good, so it went.&lt;/p&gt;

&lt;p&gt;Glitter took two rounds. The first one lay on the ribbon like a still sticker. Now each flake is fixed to the fabric, twinkles on its own beat and flares when its patch turns to the light. Then the flakes lined up in rows, so they are scattered over two layers, turned and shifted against each other.&lt;/p&gt;

&lt;p&gt;Fibre optic took three. The first version was even dark stripes. It became two layers of strands with uneven spacing, each strand with its own thickness and brightness, light gathering in patches that drift down them, and a few strands in the palette‚Äôs accent colors. Then it made the whole ribbon darker. It switched off the silk threads that give the ribbon its pale edge, dimmed the base by 10%, and far strands blurred into a grey veil. Now the strands glow over the silk threads, and the ribbon is as bright as on Silk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv61vgp9p1g90edzvx3ke.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv61vgp9p1g90edzvx3ke.webp" alt="The first fibre optic texture on a violet ribbon: even dark and light stripes over the whole band" width="800" height="500"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiefgaywoe7uw76bdqt8g.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiefgaywoe7uw76bdqt8g.webp" alt="The shipped fibre optic texture on the same violet ribbon: soft glowing strands of uneven width over a bright surface" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fibre optic on the same violet ribbon. Left: the first version. Right: the one that shipped.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Lenticular showed that some sliders did nothing. So every Surface slider was tried on each of the 23 textures. Streaks worked only on silk, and now they show only for silk and fibre optic. Texture strength did nothing above 1 on half the textures. Now 0 turns any texture off, 1 is its designed look and 2 doubles it. Pattern density was added for every texture that has a pattern.&lt;/p&gt;

&lt;p&gt;Two weeks later I saw polygons on Satin, one per triangle of the mesh, and noticed that picking a texture changed the ribbon‚Äôs base color. The light on the band was computed once per triangle. It is now computed at the corners and blended across. Every texture now keeps the average light of the silk threads, so switching textures keeps the color. In the same pass, Mercury and Liquid chrome reflected in grey and lost the ribbon‚Äôs color, and Pearl and Lenticular bleached it toward grey. All four are now lit in the palette‚Äôs colors. Chameleon and Holo film stayed as they were, because shifting the color is the point of them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdy2zt6oqrnkpygd5tanl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdy2zt6oqrnkpygd5tanl.webp" alt="A ribbon in the Mercury texture on the Sakura palette, reflecting in grey and white" width="800" height="500"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdi46ckkc4u9envrse0em.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdi46ckkc4u9envrse0em.webp" alt="The same Mercury ribbon reflecting in the palette‚Äôs pink" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Mercury on the pink Sakura palette. Left: a grey mirror. Right: the same mirror lit in pink.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe7sz5oyw6ay8parrgyx3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe7sz5oyw6ay8parrgyx3.webp" alt="Close-up of a fibre optic ribbon in green, teal and yellow strands" width="800" height="800"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg1bjcjneiv0kw0dshs72.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg1bjcjneiv0kw0dshs72.webp" alt="Close-up of a lavender ribbon with fine glitter" width="800" height="800"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Froi3pshwwhiid6lto7wc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Froi3pshwwhiid6lto7wc.webp" alt="Close-up of a gold ribbon in brushed metal" width="800" height="800"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ho7tf4jsgd7oshwbmf1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ho7tf4jsgd7oshwbmf1.webp" alt="Close-up of a coral ribbon with a pearl finish" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fibre optic, glitter, brushed metal and pearl, each from one of the presets.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Shape
&lt;/h2&gt;

&lt;p&gt;The first ribbon was a folded band whose two layers twisted apart. I asked for one band with three plain controls: width, length and thickness. Folded stayed as a second form.&lt;/p&gt;

&lt;p&gt;Then one edge of the band looked broken while the other was clean. The edge is seen side-on, and its light jumped from one row of the mesh to the next, drawing ticks across it. The edge got its own smooth light. Near one side of the band the faces sheared apart under the width twist, so the twist now holds rigid over the outer 8% of the width.&lt;/p&gt;

&lt;p&gt;The Shape folder holds the form, width, length and thickness, three twists (along the length, across the width and in depth), a falloff for each twist that sets how quickly it fades, and Wave, the size of the slow waves rolling through the band. I moved them by eye until the ribbon swept across the frame the way I wanted, copied the values out as JSON and made them the defaults. The changes look small in numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Start&lt;/th&gt;
&lt;th&gt;Default now&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Twist length&lt;/td&gt;
&lt;td&gt;-0.65&lt;/td&gt;
&lt;td&gt;-0.36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Twist width&lt;/td&gt;
&lt;td&gt;0.41&lt;/td&gt;
&lt;td&gt;0.62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Twist depth&lt;/td&gt;
&lt;td&gt;-0.58&lt;/td&gt;
&lt;td&gt;-0.19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Width falloff&lt;/td&gt;
&lt;td&gt;0.70&lt;/td&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Length&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;432&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thickness&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;2.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj5g7djjx9e8zm0gi65iu.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj5g7djjx9e8zm0gi65iu.webp" alt="A blue silk ribbon with the starting shape values: a tight twist that turns the band edge-on in the middle" width="800" height="500"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fevir1gslqylhm8n0y6l6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fevir1gslqylhm8n0y6l6.webp" alt="The same ribbon with the tuned values: a wide band that sweeps diagonally across the frame" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Same palette, same seed. Left: the starting values. Right: mine, now the default.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Turning the ribbon got its own controls in View: Rotate X, Y and Z sliders, and an axes widget in the corner. Clicking the end of an axis turns that axis toward you, the short way round. Randomize keeps the shape near the default composition. The twists, falloffs and wave move within narrow ranges (Twist width 0.25 to 1, Wave -12 to -3) and the seed does the rest. With wide ranges the ribbon slid to the edge or filled the whole frame.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8muusyfw8ckd16s0lx2g.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8muusyfw8ckd16s0lx2g.webp" alt="The Ribbonlab panel with the Shape folder open: Form Single or Folded, Width, Length, Thickness, three twists, three falloffs and Wave, beside a violet ribbon" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Shape folder on the default ribbon.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Settings
&lt;/h2&gt;

&lt;p&gt;The panel has seven folders in the order people tune a background: Color, Adjustments, Shape, Surface, Effects, Motion and View. On the first visit only Color is open. Brightness first sat in Effects. It changes the color, so it moved to a new Adjustments folder with saturation, contrast and hue. Each effect is a switch, and its sliders sit in the same card only while it is on.&lt;/p&gt;

&lt;p&gt;There are 53 palettes in eight groups by hue, each with two to six colors, and you can add or remove colors within that range. Picking a palette changes only the colors. Every other slider stays where you put it, so trying palettes never throws work away.&lt;/p&gt;

&lt;p&gt;The white palettes exposed a setting nobody could see. A grey ribbon came out blue. One line in the color shader cooled every shaded side: red dropped to 45% and blue only to 75%. It gave the blue and violet ribbons their depth and made grey and warm ones look dirty. I made it a slider, Shadow warmth, with the usual cool shadows in the middle, deeper blue to the left and warm to the right. Each palette sets it when picked. Greys and warm palettes go neutral.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1syy7rwbn24gfknxpln.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1syy7rwbn24gfknxpln.webp" alt="A white Cobweb ribbon whose shaded folds come out blue" width="800" height="500"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4gic5jfnj33ldnr9wcqt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4gic5jfnj33ldnr9wcqt.webp" alt="The same white ribbon with neutral grey shading" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cobweb, a white palette. Left: the old fixed cool shadows. Right: the neutral shadows it now sets itself.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Neutral values also sat in odd places on their sliders. Brightness ran from 0.3 to 2, so its neutral 1.0 sat at 40% of the track. Ranges with a neutral point are now symmetric, and the neutral value sits in the middle. The slider called Warmth turned out to mix in the accent colors, so it is called Accent mix now.&lt;/p&gt;

&lt;p&gt;Randomize works on one folder with the dice on its header, or on everything at once with G. Each slider draws from a range where results look good instead of anywhere on its track: brightness 0.92 to 1.12, saturation 0.85 to 1.3. Loud settings move only sometimes: hue and rainbow in 15% of rolls, iridescence in 30%. Randomize all keeps your texture, because a texture suits some looks and not others. The Surface dice picks textures by weight, Silk most often, and skips Liquid chrome, Marble and Sequins, which gave muddy results at random. Randomize and Reset both show an Undo.&lt;/p&gt;

&lt;p&gt;Presets went through ten placements, each prototyped in the working editor: a window of rendered ribbons, a folder in the panel, a stage with the preset name, a card deck and more. Each one covered the ribbon or competed with the panel. What shipped takes no space. The left and right arrow keys flip ten presets with a crossfade, and a small pill names each one, then hides.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2axjrt800cgyt4ko97s1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2axjrt800cgyt4ko97s1.webp" alt="Prototype: a Looks window over the dimmed editor with ten rendered ribbons in two rows" width="800" height="500"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbvyxcsdvax7ogmg61m1c.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbvyxcsdvax7ogmg61m1c.webp" alt="Prototype: a stage with the preset name Violet in large type and arrows under it" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Two of the ten preset prototypes: a window of ribbons and a stage.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A background video has to repeat without a jump. With Seamless loop on, the wave blends into the same moment one loop earlier, so the end meets the start. Glitter, sequins and fibre optic run on their own timers, and those blend the same way. Auto-turn makes whole turns per loop. The first export recorded the canvas in real time, added a stray frame at the start and lost frames at the end. Export now renders the loop frame by frame with exact timestamps, so a two-second loop is exactly 60 frames. I asked why export offered two or three loops of something that repeats anyway, so a loop exports as one loop, with its length taken from the Loop length slider in Motion. GIF is offered only for loops.&lt;/p&gt;

&lt;p&gt;Export also gives code, and the code went through four designs. A standalone HTML file handed out the whole editor. An npm package would have published the renderer for anyone to copy. A short id with a script tag worked until Framer turned out to strip script tags from embeds. An iframe of the ribbon‚Äôs own page shipped: it loads in Webflow, Framer, WordPress and Shopify, React gets a script snippet and Notion a plain link. Each player version is frozen once it ships, so a ribbon on someone‚Äôs site never changes when I change the look. I also built a lock that kept each ribbon to named sites, and removed it, because it made the first use harder for everyone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4g7mthb8gijxjiaa3p3y.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4g7mthb8gijxjiaa3p3y.webp" alt="The Ribbonlab export window on the Code tab: HTML, React and URL options, background Hide or Show, a code preview with an iframe snippet and a Copy code button" width="800" height="702"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Export, Code tab. One snippet, with a note on where it goes in each site builder.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Speed
&lt;/h2&gt;

&lt;p&gt;PageSpeed Insights first reported Error on every metric. Its test browser has no GPU, and opening WebGL in software froze the page for one to two seconds. The page now checks the GPU in a background worker, shows a still of the ribbon where there is no GPU, and starts the renderer after the page has painted.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PageSpeed&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mobile&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why this matters for your product
&lt;/h2&gt;

&lt;p&gt;Claude Code wrote most of the lines. The decisions in this piece are mine: what each texture should look like and when it was good enough, which sliders exist and in what order, which ranges random values may take, and what to cut. Each one was judged in the running product, so ten versions of one decision took a day.&lt;/p&gt;

&lt;p&gt;I work the same way with teams, from deciding what to build to a working release. See &lt;a href="https://ayurchenko.xyz/fractional-product-design" rel="noopener noreferrer"&gt;Fractional Product Design&lt;/a&gt;, start with a &lt;a href="https://ayurchenko.xyz/product-diagnosis" rel="noopener noreferrer"&gt;Product Diagnosis&lt;/a&gt;, or &lt;a href="https://ayurchenko.xyz/mailto:andriy.dsgn@gmail.com" rel="noopener noreferrer"&gt;write to me&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Ribbonlab free?
&lt;/h3&gt;

&lt;p&gt;Yes. The editor, every export and the embed code are free, and there is no account to create.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I add an animated 3D background to my website?
&lt;/h3&gt;

&lt;p&gt;Open &lt;a href="https://ribbonlab.ayurchenko.xyz/" rel="noopener noreferrer"&gt;Ribbonlab&lt;/a&gt;, tune a ribbon, then choose Export and Code. The HTML snippet goes into a code embed in Webflow, Framer, WordPress or Shopify, React projects use the React snippet, and the &lt;a href="https://ribbonlab.ayurchenko.xyz/guides/" rel="noopener noreferrer"&gt;guides&lt;/a&gt; show each builder step by step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which textures does Ribbonlab have?
&lt;/h3&gt;

&lt;p&gt;Twenty-three, in three groups. Fabric: silk, satin, velvet, twill, matte and latex. Metal and shine: mercury, liquid chrome, brushed metal, chameleon, holo film, lenticular, fibre optic, pearl, glitter and sequins. Pattern: moir√©, guilloch√©, marble, topographic, leopard, zebra and polygonal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I export a seamless looping video?
&lt;/h3&gt;

&lt;p&gt;Yes. Turn on Seamless loop in the Motion folder, set the loop length, and export one loop as MP4, WebM or GIF. WebM keeps a transparent background.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long did Ribbonlab take to build?
&lt;/h3&gt;

&lt;p&gt;A few evenings, in spare time, a little at a time rather than in one stretch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who made Ribbonlab?
&lt;/h3&gt;

&lt;p&gt;Andriy Yurchenko, a product designer and design engineer. He made the product decisions, designed and built it on his own, with Claude Code writing most of the code, and takes products for teams from the first decision to a working release.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article first appeared on &lt;a href="https://ayurchenko.xyz/writing/ribbonlab" rel="noopener noreferrer"&gt;ayurchenko.xyz&lt;/a&gt;. I am Andriy Yurchenko, a product designer and design engineer. More of my writing is at &lt;a href="https://ayurchenko.xyz/writing" rel="noopener noreferrer"&gt;ayurchenko.xyz/writing&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webgl</category>
      <category>design</category>
      <category>ai</category>
    </item>
    <item>
      <title>Design Systems for Coding Agents: 11 Builds, Measured</title>
      <dc:creator>Andriy Yurchenko</dc:creator>
      <pubDate>Fri, 13 Nov 2026 19:31:52 +0000</pubDate>
      <link>https://dev.to/andyur/design-systems-for-coding-agents-11-builds-measured-1l0d</link>
      <guid>https://dev.to/andyur/design-systems-for-coding-agents-11-builds-measured-1l0d</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://ayurchenko.xyz/writing/design-systems-for-coding-agents" rel="noopener noreferrer"&gt;ayurchenko.xyz&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I gave one AI agent the same brief again and again: with no design system, on shadcn/ui, and on a product whose look had been decided before the first screen. Then I measured every element it shipped. The first pass turned out to be the easy part. What a system is for now starts after it.&lt;/p&gt;

&lt;p&gt;I have built a design system for every product I have worked on, and over 14 years that is dozens of products. The early ones were Figma libraries and pages of documentation for people. The recent ones live in code, and I implement the components in Storybook myself, because the designer who writes the components is the first to see every gap in the system.&lt;/p&gt;

&lt;p&gt;Now agents write a lot of the interface. Founders ask me a fair question. If Claude Code or Cursor builds a screen in minutes, and shadcn/ui gives everyone a component library for free, what is a design system still for? I wanted to answer with numbers instead of an opinion, so I ran two tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The product is Halden, a dashboard for a coffee roaster in Portland with six cafes and a wholesale line. I made it as a test bed for a review tool: an agent built it in one evening on a visual direction I set first, a token file and a small kit, with no design system behind it. The tool and that evening are in &lt;a href="https://ayurchenko.xyz/writing/screen-canvas" rel="noopener noreferrer"&gt;A canvas for reviewing AI-built UI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The agent had six screens to build: the home dashboard, a list of transactions, a customer record, the business settings form, a new wholesale order, and an order with its timeline. Every build got the same written brief, the same data and the same model, Claude Sonnet in Claude Code, in one pass. None of them saw a screenshot of Halden or a line of its CSS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No system, twice.&lt;/strong&gt; React and Tailwind, no component library, no tokens. What a founder gets from Cursor or Claude Code with a prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;shadcn/ui, twice.&lt;/strong&gt; A fresh shadcn setup with its default theme, the stack Lovable starts from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Halden, a decided look with a thin system.&lt;/strong&gt; Four of the six screens come from Halden itself, built in one evening as a test bed for a review tool on a token file and a small kit: no rules for states, no component descriptions, no checks. Halden had no wholesale section, so the other two are the wholesale screens an agent added in the growth test below, from the build that did have a rules file, a catalog and an audit. I took the versions before any review fixes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For every screen a script opened the page in headless Chrome at 1440 pixels and read the computed style of every element: how many different buttons and fields there are, which values fall outside the build‚Äôs own scale, which looks appear only once. Then it changed the main color, the corner radius and the density in one place, and counted what followed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers say
&lt;/h2&gt;

&lt;p&gt;Less than I expected. On the first pass every build came out reasonably consistent.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Across six screens&lt;/th&gt;
&lt;th&gt;No system (2 runs)&lt;/th&gt;
&lt;th&gt;shadcn/ui (2 runs)&lt;/th&gt;
&lt;th&gt;Halden (a decided look, thin system)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Different button looks&lt;/td&gt;
&lt;td&gt;9 / 8&lt;/td&gt;
&lt;td&gt;13 / 12&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Different field looks&lt;/td&gt;
&lt;td&gt;2 / 3&lt;/td&gt;
&lt;td&gt;4 / 3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Looks used only once&lt;/td&gt;
&lt;td&gt;15 / 17&lt;/td&gt;
&lt;td&gt;17 / 15&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Values off the build‚Äôs own scale&lt;/td&gt;
&lt;td&gt;7 / 10&lt;/td&gt;
&lt;td&gt;6 / 6&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw Tailwind colors around the theme&lt;/td&gt;
&lt;td&gt;no theme&lt;/td&gt;
&lt;td&gt;34 / 24&lt;/td&gt;
&lt;td&gt;no Tailwind&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main color change followed&lt;/td&gt;
&lt;td&gt;100% / 100%&lt;/td&gt;
&lt;td&gt;100% / 100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Radius change followed&lt;/td&gt;
&lt;td&gt;100% / 76%&lt;/td&gt;
&lt;td&gt;100% / 100%&lt;/td&gt;
&lt;td&gt;79%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every button has a disabled look&lt;/td&gt;
&lt;td&gt;no / no&lt;/td&gt;
&lt;td&gt;yes / yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to build&lt;/td&gt;
&lt;td&gt;6.5 / 9.0 min&lt;/td&gt;
&lt;td&gt;6.0 / 5.7 min&lt;/td&gt;
&lt;td&gt;one evening, with the kit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The builds with no system did not stay without one. Before the first screen, both agents wrote their own small block of tokens and a file of buttons, fields and cards. Tailwind v4 hands them variables for spacing and radius out of the box, so even these builds changed density in one place.&lt;/p&gt;

&lt;p&gt;shadcn/ui shipped the most complete states: every button had a disabled look and a loading state. Halden lost on exactly the things a system is for. Five button heights, 21 values off its scale, no disabled look, the browser‚Äôs default focus ring. That is what a look without a system produces, and it is the point of this article: decisions give a product its face, and a system is what keeps that face consistent and complete.&lt;/p&gt;

&lt;p&gt;So the first pass is no longer where a product goes wrong. An agent on a modern stack gets the basics close enough by itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers miss
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8bx72lvf0qcuxxbzx54p.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8bx72lvf0qcuxxbzx54p.webp" alt="Halden home dashboard: light, one sky-blue accent on the chart" width="800" height="500"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqexrcgeg5ia56iu89px5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqexrcgeg5ia56iu89px5.webp" alt="First shadcn/ui build of the same dashboard: grey and black" width="800" height="500"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff8cxamcstsvk9uoh1m3x.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff8cxamcstsvk9uoh1m3x.webp" alt="Second shadcn/ui build: nearly identical to the first" width="800" height="500"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7q7pqty48h3jrlmut75s.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7q7pqty48h3jrlmut75s.webp" alt="Build with no system: a brown palette the agent chose itself" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same dashboard in four builds. Top: Halden, then the first shadcn/ui build. Bottom: the second shadcn/ui build, then the build with no system. Screenshots at 1440 pixels, top of the page.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Put the screens next to each other and the measurements stop being the interesting part. The two shadcn/ui builds are the same product twice: the same grey, the same black bars, the same weight on every card. Any other company that ships the default theme gets this face too, because nobody decided anything about it.&lt;/p&gt;

&lt;p&gt;The build with no system picked a brown ‚Äúcoffee‚Äù palette on its own. That is a choice, and the agent made it, not the company.&lt;/p&gt;

&lt;p&gt;Halden has a look because someone decided it. One blue accent, used only where it carries meaning. A quiet grey for everything else. Density set for a manager who checks numbers between two cafes. None of that comes in a component library. It is a set of decisions, and tokens are the way those decisions get written down so that an agent can follow them.&lt;/p&gt;

&lt;p&gt;Where the default theme had no decision, the agents filled the gap themselves. shadcn‚Äôs theme has no color for success or warning, so both shadcn builds styled their statuses with raw Tailwind colors: 34 classes in one, 24 in the other, all out of reach of the next theme change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmm49zjvgvlw9wzxo0hi4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmm49zjvgvlw9wzxo0hi4.webp" alt="Halden new wholesale order form" width="800" height="500"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhhti88rzaanwh3eqfec5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhhti88rzaanwh3eqfec5.webp" alt="New order form in the shadcn/ui build" width="800" height="500"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmp5tjhhrhcnmn3w4plv8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmp5tjhhrhcnmn3w4plv8.webp" alt="New order form in the build with no system" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The new order form in three builds: Halden, shadcn/ui, no system. Screenshots at 1440 pixels, top of the page.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A short brief builds a different product every time
&lt;/h2&gt;

&lt;p&gt;I ran a control. The same two stacks, twice each, got a one-paragraph brief, the way founders often write one: six screens and their addresses, the data file, and ‚Äúmake it look good‚Äù.&lt;/p&gt;

&lt;p&gt;The screens looked fine. The agents chose palettes, added cards nobody asked for, and one rewrote the shadcn theme to match a brand it imagined. But a third of the concrete decisions disappeared: of 79 items in the detailed brief, the builds kept between 43 and 50. No build had a loading or an error state. Two runs of the same stack disagreed about the product itself, bars against a line, today against the last seven days, different cards on the dashboard. Looks used only once rose to between 23 and 29 in three of the four builds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxjzdx13juglt6idu27px.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxjzdx13juglt6idu27px.webp" alt="shadcn/ui build from a one-paragraph brief, first run" width="800" height="500"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcfgt9amdmigjbirwbwrl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcfgt9amdmigjbirwbwrl.webp" alt="The same stack and paragraph, second run, with a different dashboard" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Same stack, same paragraph, two runs. Screenshots at 1440 pixels, top of the page.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is the part an agent does not invent: the decisions. The detailed brief did not make the screens prettier. It made them complete, and the same twice. A design system is that brief, written once, for every screen the product will ever have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where drift starts: the second screen, and the fiftieth
&lt;/h2&gt;

&lt;p&gt;The first test I ran was about growth. Three agents added the same six-screen wholesale section to Halden, which already had its token file and component kit in the code. One got only the brief. One also got written guidelines, the kind of page a team keeps in Notion. One got the system in a form an agent reads: a rules file, a catalog that says what each component is for and when not to use it, and an audit it had to run until it reported nothing.&lt;/p&gt;

&lt;p&gt;On screen, the three sections looked almost the same. The agents copied the existing screens, mistakes included: two of them took a hard-coded field height from Halden‚Äôs settings form straight into their new code. The difference was underneath:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the new section&lt;/th&gt;
&lt;th&gt;Brief only&lt;/th&gt;
&lt;th&gt;Plus written guidelines&lt;/th&gt;
&lt;th&gt;Plus rules, catalog, audit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw values in new code&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New CSS classes&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines of CSS added&lt;/td&gt;
&lt;td&gt;233&lt;/td&gt;
&lt;td&gt;187&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where the styles went&lt;/td&gt;
&lt;td&gt;a stylesheet for this section&lt;/td&gt;
&lt;td&gt;a stylesheet for this section&lt;/td&gt;
&lt;td&gt;shared components, recorded in the catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent time&lt;/td&gt;
&lt;td&gt;8.1 min&lt;/td&gt;
&lt;td&gt;7.3 min&lt;/td&gt;
&lt;td&gt;4.4 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first two forked the product. Each wrote a private stylesheet for one section, with about thirty classes nobody else can reuse and values that will not follow the next token change. Every screen looks right today. The cost arrives with the next fifty screens, when nobody knows which of three similar buttons is the real one.&lt;/p&gt;

&lt;p&gt;The third agent added the missing form fields as components of the whole system and wrote them into the catalog, so the next agent finds them. It was also the fastest. A system that answers the agent‚Äôs questions saves the time it would otherwise spend guessing.&lt;/p&gt;

&lt;p&gt;One more detail. The written guidelines said cards have 16 pixel corners. The code said 24. The agent followed the code. Documentation that lives apart from the code drifts, and an agent always believes the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I put in a system now that agents read it
&lt;/h2&gt;

&lt;p&gt;What changed in the last two years is the reader. A system used to be documentation for designers and engineers. Now it is also the input a coding agent builds from, and the thing its output gets checked against. These are the parts I add for that reader.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code is the source of truth.&lt;/strong&gt; Tokens and components live in the repository and in Storybook. Figma is where the design work happens and gets synced back. When the two disagree, code wins, and the sync merges changes instead of overwriting them, so values that exist only in code survive it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Names an agent reads the way people do.&lt;/strong&gt; If a list row lives inside a component called ‚Äúmenu‚Äù, every agent will build menus out of it. Renaming is a design decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descriptions written for an agent.&lt;/strong&gt; Every component says what it is for, when not to use it and what to use instead. The intent lives in one place. A second copy of the same text goes stale, and a stale description misleads an agent more than no description at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rules file, tested blind.&lt;/strong&gt; An agent builds a screen without being told about the system. Then I plant violations in clean code and check that the rules catch every one of them, with no false alarms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;States for every component.&lt;/strong&gt; Hover, focus, disabled, loading, error, empty. Halden skipped them, and the table above shows what that costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Figma file an agent can read.&lt;/strong&gt; An agent checks layer names, component descriptions and token bindings, so that design-to-code reads intent rather than geometry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checks on what ships.&lt;/strong&gt; A token audit that flags raw values in new code, and an inventory of the running product that finds what drifted (&lt;a href="https://ayurchenko.xyz/writing/screen-canvas" rel="noopener noreferrer"&gt;how I review it&lt;/a&gt;). The findings go back to the agent as tasks. Whether a person can finish a task is a separate check: the &lt;a href="https://ayurchenko.xyz/writing/ux-audit-agent" rel="noopener noreferrer"&gt;UX audit agent&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rule for anything new.&lt;/strong&gt; A new look becomes a variant of an existing component. New behavior gets a component of its own. A new job gets a new component built from the old parts. Anything else turns into a one-off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is what the third agent in the growth test read before it touched the wholesale section, shortened. The rules file says where values come from and what to do when nothing fits:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AGENTS.md, excerpt&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Read this file and COMPONENTS.md before you write any UI. These rules are checked by npm run audit.

Source of truth
&lt;span class="k"&gt;- &lt;/span&gt;styles/tokens.css is the only source of values. Every color, size, space, radius, font size, weight, shadow and duration in new code is a var(--token). No hex, rgb, hsl or oklch literals, and no px values except 0 and 1px borders.
&lt;span class="k"&gt;- &lt;/span&gt;Do not add tokens. If a value you need has no token, use the nearest one and say so in your final message.

When nothing fits
&lt;span class="k"&gt;- &lt;/span&gt;Add one component to styles/components.css, built only from tokens, plus an entry in COMPONENTS.md. Then use it everywhere it applies.
&lt;span class="k"&gt;- &lt;/span&gt;Never style an element for one screen only. If a screen needs a look that no component has, that look is a component.

Before you finish
Run npm run audit. Fix every finding, then run it again. Repeat until it reports 0 findings, or explain in your final message why a finding must stay.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catalog answers the question the other two agents answered by guessing: which piece is the right one. Every row says what it is for and what to use instead:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;COMPONENTS.md, four of 21 rows&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;badge(label): every status; tone comes from the tones map. Not for categories or counts.
stat({label, value, change, note}): one headline number with an optional change. Not for lists of numbers (use table or .facts).
segmented(q, key, options): switching between 2 to 5 views of the same list. Not for filters with many values (use chip).
timeline([{label, note, state}]): the steps one record has gone through. Not for steps the user fills in (use .stepper).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither file is long. The rules file is 23 lines and the catalog 29. What makes them work is the last rule: the audit runs until it reports nothing, so the agent cannot finish with a raw value in the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your own system, shadcn‚Äôs skeleton, or shadcn as it is
&lt;/h2&gt;

&lt;p&gt;shadcn/ui is a good start, and the test shows why: its components came with the most complete states of any build. The question is what you need on top of it, and that depends on the time you have and what the product has to do.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;shadcn as it is&lt;/strong&gt; fits a prototype, an internal tool, or a product where looking like everyone else costs nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;shadcn‚Äôs skeleton, reworked&lt;/strong&gt; fits most startups. Keep the behavior and replace the decisions: your color roles, including success and warning, your density, your type, your corners, and component descriptions written for agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A system of your own&lt;/strong&gt; fits when the product‚Äôs look is part of what you sell, when several products or brands share one system, or when the team will grow past the point where anyone remembers the exceptions. In my experience shadcn‚Äôs tokens and interactions are not consistent enough from one component to the next for that, so I build from one token logic and one set of interaction rules. It takes longer at the start. Afterwards the team, and its agents, read the logic instead of memorising exceptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whichever you choose, the test that decides it is the same, and it takes more than one round: give an agent a real task, look at what it shipped, find where it guessed, and fix the system there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this comes from
&lt;/h2&gt;

&lt;p&gt;I have built a design system for every product I worked on. The ones I can show, and what they changed, are in the case studies: &lt;a href="https://ayurchenko.xyz/work/truefi" rel="noopener noreferrer"&gt;TrueFi&lt;/a&gt;, &lt;a href="https://ayurchenko.xyz/work/deva" rel="noopener noreferrer"&gt;Deva&lt;/a&gt; and &lt;a href="https://ayurchenko.xyz/work/10planet" rel="noopener noreferrer"&gt;10Planet&lt;/a&gt;, &lt;a href="https://ayurchenko.xyz/work/equi" rel="noopener noreferrer"&gt;Equi&lt;/a&gt; and &lt;a href="https://ayurchenko.xyz/work/fully-ai" rel="noopener noreferrer"&gt;Fully AI&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do this week
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Take an inventory of what your product ships: every button, every field, every value outside your tokens. Count them.&lt;/li&gt;
&lt;li&gt;Write down the decisions an agent keeps guessing: color roles including success and warning, density, type, corners.&lt;/li&gt;
&lt;li&gt;Give each component a description: what it is for, when not to use it, what to use instead.&lt;/li&gt;
&lt;li&gt;Add a rules file, then test it: let an agent build one screen and see where it guessed.&lt;/li&gt;
&lt;li&gt;Check new code for raw values before every merge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you would rather have someone do the first step, that is what the &lt;a href="https://ayurchenko.xyz/design-systems" rel="noopener noreferrer"&gt;Consistency Review&lt;/a&gt; is.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I ran the test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Eleven one-pass builds in total: four from the detailed brief, four from the one-paragraph brief, two of each per stack, plus three in the growth test, one per condition. Claude Sonnet, in the background, checked with a production build only. The agents were not told it was a test.&lt;/li&gt;
&lt;li&gt;The agents ran with my general coding preferences, which already favor CSS variables. That makes the builds with no system better than a typical one, so the gaps here are conservative.&lt;/li&gt;
&lt;li&gt;Halden‚Äôs brief did not ask for interaction states. The other builds‚Äô briefs did.&lt;/li&gt;
&lt;li&gt;Two runs per stack is a small sample. The spread between two runs of one stack was sometimes as large as the spread between stacks.&lt;/li&gt;
&lt;li&gt;A ‚Äúlook‚Äù is an exact match of computed styles, so two nearly identical buttons count as two.&lt;/li&gt;
&lt;li&gt;The rebrand check changed the main color, multiplied the corner radius by 1.6 and the density by 1.25, and counted the share of elements that moved.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cheap and expensive
&lt;/h2&gt;

&lt;p&gt;Anyone can build a product now. An agent will give a founder a working app in an evening, and every screen in it will look reasonable. That is the new floor, and most products will stand on it, in the stack‚Äôs default theme or a palette the agent picked, looking like every other app built the same way.&lt;/p&gt;

&lt;p&gt;What lifts a product off that floor is a set of decisions the agent will not make: which states every control has, which color means success, how dense a table can get for the person who reads it between two meetings, where the one accent goes and where it never goes, which of three similar buttons is the real one. In both tests, the builds that had those decisions written down came out complete and the same twice. The ones that did not were reasonable and different every time.&lt;/p&gt;

&lt;p&gt;Making those decisions well takes taste and having watched real people use real products many times. Writing them down so an agent follows them is now part of the same job. Agents made building cheap; the decisions are the expensive part.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article first appeared on &lt;a href="https://ayurchenko.xyz/writing/design-systems-for-coding-agents" rel="noopener noreferrer"&gt;ayurchenko.xyz&lt;/a&gt;. I am Andriy Yurchenko, a product designer and design engineer. More of my writing is at &lt;a href="https://ayurchenko.xyz/writing" rel="noopener noreferrer"&gt;ayurchenko.xyz/writing&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>designsystems</category>
      <category>ai</category>
      <category>frontend</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
