<?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: Anatolii Skuba</title>
    <description>The latest articles on DEV Community by Anatolii Skuba (@anatoliiskuba).</description>
    <link>https://dev.to/anatoliiskuba</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%2F4062976%2F86a036ef-111a-45c3-ba27-48b69b239af0.jpeg</url>
      <title>DEV Community: Anatolii Skuba</title>
      <link>https://dev.to/anatoliiskuba</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anatoliiskuba"/>
    <language>en</language>
    <item>
      <title>Why your five shadows don't look like one system</title>
      <dc:creator>Anatolii Skuba</dc:creator>
      <pubDate>Tue, 04 Aug 2026 18:41:55 +0000</pubDate>
      <link>https://dev.to/anatoliiskuba/why-your-five-shadows-dont-look-like-one-system-147a</link>
      <guid>https://dev.to/anatoliiskuba/why-your-five-shadows-dont-look-like-one-system-147a</guid>
      <description>&lt;p&gt;Nobody hand-tunes one shadow and gets it wrong. The problem starts at five.&lt;/p&gt;

&lt;p&gt;You nudge each elevation level until it looks right on its own, and the finished set looks like five shadows from five different rooms. Here's why that happens, and what to derive them from instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break #1: the angle drifts
&lt;/h2&gt;

&lt;p&gt;In a real scene there is one light, so every object casts its shadow in the same direction. Break just this and the eye notices immediately — not as "the shadows are inconsistent" but as something closer to nausea. A card whose shadow falls down-right next to a modal whose shadow falls straight down reads as two photographs pasted together.&lt;/p&gt;

&lt;p&gt;Drift is almost never a decision. It's what happens when level 4 is tuned three weeks after level 1, in a different file, against a different background. The fix isn't discipline, it's removing the opportunity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;shadowDirection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;azimuth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shadows fall &lt;em&gt;away&lt;/em&gt; from the light, which is the whole content of that line. Every level then shares one azimuth, and drift becomes unrepresentable rather than merely discouraged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break #2: the blur ramp is arbitrary
&lt;/h2&gt;

&lt;p&gt;Ask five designers for elevation blurs and you'll get 4/8/16/24/32, or 2/6/12/24/48, or whatever landed. Those are guesses dressed as a scale.&lt;/p&gt;

&lt;p&gt;Physically, blur isn't a free parameter — it's governed by how far away the light is. A distant light is effectively a point source and casts a crisp edge; a nearby one wraps around the object and smears it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;penumbra&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;blur&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;elevation&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1.4&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="nx"&gt;penumbra&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things follow that hand-tuning tends to miss. First, blur is tied to elevation — something floating higher is necessarily blurrier, so you cannot pick blur per level independently and stay coherent. Second, blur never reaches zero: a raised element sitting on a hard-edged shadow reads as a sticker, not as an object above a surface.&lt;/p&gt;

&lt;p&gt;The levels themselves come from one number too. Elevation grows geometrically, which is what makes the steps feel evenly spaced rather than crowded at the bottom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;elevation(n) = base × growth^(n-1)

base 4px, growth 1.7  →  4 · 6.8 · 11.6 · 19.7 · 33.4 px
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Break #3: opacity stacks into grey
&lt;/h2&gt;

&lt;p&gt;This is the one people misdiagnose most often. A shadow that looks dirty is almost never a blur problem — it's an opacity problem. One layer at 0.30 reads as a grey box under your card. Two or three layers between roughly 0.06 and 0.16 read as light. Same darkness, completely different impression.&lt;/p&gt;

&lt;p&gt;A real shadow isn't one blurred rectangle. Close to the object there's a dense &lt;em&gt;umbra&lt;/em&gt;; further out it fades into a wide, faint &lt;em&gt;penumbra&lt;/em&gt;. Stacking a few layers with growing blur and falling opacity approximates that falloff.&lt;/p&gt;

&lt;p&gt;One constraint matters more than it looks: keep the whole ramp inside a band. Below about &lt;strong&gt;0.04&lt;/strong&gt; a shadow is invisible and you're shipping dead CSS; above about &lt;strong&gt;0.24&lt;/strong&gt; it stops reading as light. Clamping to that band is what stops a design system from slowly darkening as people "make it a bit more visible" over two years.&lt;/p&gt;

&lt;p&gt;And higher things cast fainter shadows — the same light spreads across more area, so density per pixel drops. A square root keeps level 5 from vanishing while still fading it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;brightness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;brightness&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="err"&gt;√&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The ramp, ready to paste
&lt;/h2&gt;

&lt;p&gt;Everything above, run with one light — azimuth 0°, distance 200, base elevation 4px, brightness 0.6, three layers per level. Generated output, not a hand-picked example:&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;--elevation-1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;1.3px&lt;/span&gt; &lt;span class="m"&gt;2.6px&lt;/span&gt; &lt;span class="n"&gt;rgba&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="m"&gt;0&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="m"&gt;0.16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                 &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;2.4px&lt;/span&gt; &lt;span class="m"&gt;6.5px&lt;/span&gt; &lt;span class="n"&gt;rgba&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="m"&gt;0&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="m"&gt;0.127&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                 &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;3.6px&lt;/span&gt; &lt;span class="m"&gt;10.4px&lt;/span&gt; &lt;span class="n"&gt;rgba&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="m"&gt;0&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="m"&gt;0.094&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--elevation-2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;2.1px&lt;/span&gt; &lt;span class="m"&gt;4.4px&lt;/span&gt; &lt;span class="n"&gt;rgba&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="m"&gt;0&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="m"&gt;0.125&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                 &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;4.1px&lt;/span&gt; &lt;span class="m"&gt;11px&lt;/span&gt; &lt;span class="n"&gt;rgba&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="m"&gt;0&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="m"&gt;0.102&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                 &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;6.1px&lt;/span&gt; &lt;span class="m"&gt;17.7px&lt;/span&gt; &lt;span class="n"&gt;rgba&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="m"&gt;0&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="m"&gt;0.078&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--elevation-3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;3.7px&lt;/span&gt; &lt;span class="m"&gt;7.5px&lt;/span&gt; &lt;span class="n"&gt;rgba&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="m"&gt;0&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="m"&gt;0.109&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                 &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;7px&lt;/span&gt; &lt;span class="m"&gt;18.8px&lt;/span&gt; &lt;span class="n"&gt;rgba&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="m"&gt;0&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="m"&gt;0.09&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                 &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;10.4px&lt;/span&gt; &lt;span class="m"&gt;30.2px&lt;/span&gt; &lt;span class="n"&gt;rgba&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="m"&gt;0&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="m"&gt;0.071&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--elevation-4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;6.2px&lt;/span&gt; &lt;span class="m"&gt;12.8px&lt;/span&gt; &lt;span class="n"&gt;rgba&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="m"&gt;0&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="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                 &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt; &lt;span class="m"&gt;32px&lt;/span&gt; &lt;span class="n"&gt;rgba&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="m"&gt;0&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="m"&gt;0.083&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                 &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;17.7px&lt;/span&gt; &lt;span class="m"&gt;51.2px&lt;/span&gt; &lt;span class="n"&gt;rgba&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="m"&gt;0&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="m"&gt;0.067&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--elevation-5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;10.5px&lt;/span&gt; &lt;span class="m"&gt;21.7px&lt;/span&gt; &lt;span class="n"&gt;rgba&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="m"&gt;0&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="m"&gt;0.094&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                 &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;20.3px&lt;/span&gt; &lt;span class="m"&gt;54.3px&lt;/span&gt; &lt;span class="n"&gt;rgba&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="m"&gt;0&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="m"&gt;0.079&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                 &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;30.1px&lt;/span&gt; &lt;span class="m"&gt;86.8px&lt;/span&gt; &lt;span class="n"&gt;rgba&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="m"&gt;0&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="m"&gt;0.064&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;Notice what the numbers do across levels. Offset and blur grow together, the top layer's opacity falls from 0.16 to 0.094, and the direction never changes. That last part is the point: the set is one light photographed five times, not five shadows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Figma trap: a shadow can't be a variable
&lt;/h2&gt;

&lt;p&gt;If you're planning to hold this scale as Figma variables, check the type list first. Figma variables resolve to exactly four types — &lt;code&gt;BOOLEAN&lt;/code&gt;, &lt;code&gt;COLOR&lt;/code&gt;, &lt;code&gt;FLOAT&lt;/code&gt; and &lt;code&gt;STRING&lt;/code&gt;. There is no shadow type and no effect type. This is a platform limit, not a gap in your setup, and it's worth knowing &lt;em&gt;before&lt;/em&gt; you design the token structure.&lt;/p&gt;

&lt;p&gt;So the shareable primitive for a shadow system has to be an &lt;strong&gt;effect style&lt;/strong&gt; — that's what publishes to a library and what components can reference. Variables can carry the numeric elevation values (4, 6.8, 11.6…) as FLOATs, but they cannot carry the shadow itself.&lt;/p&gt;

&lt;p&gt;There's a subtler trap next to it. It's tempting to bind the shadow colour to a COLOR variable so the system is themeable. Don't — a COLOR variable carries its own alpha, and one shared alpha across a three-layer stack flattens exactly the opacity ramp that made the shadow look like light. You'd get the grey box back, this time by architecture.&lt;/p&gt;




&lt;p&gt;I build &lt;a href="https://www.figma.com/community/plugin/1664402563197365064" rel="noopener noreferrer"&gt;Elevation&lt;/a&gt;, a Figma plugin that does this maths for you: drag one light source, get the five-level scale, export it as effect styles, CSS custom properties, a Tailwind &lt;code&gt;boxShadow&lt;/code&gt; config or W3C design tokens. The editor and applying shadows are free.&lt;/p&gt;

&lt;p&gt;The longer version of this post is &lt;a href="https://localeproof.com/blog/figma-shadow-system/" rel="noopener noreferrer"&gt;on my blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>css</category>
      <category>design</category>
      <category>figma</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
