<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged css</title>
<link>https://coderlegion.com/tag/css</link>
<description>Powered by Question2Answer</description>
<item>
<title>A hash-based CSP has no room for a syntax highlighter</title>
<link>https://coderlegion.com/32699/a-hash-based-csp-has-no-room-for-a-syntax-highlighter</link>
<description>&lt;p&gt;The Content Security Policy on this site allows scripts by SHA-256 hash. Not &lt;code&gt;unsafe-inline&lt;/code&gt;, not a nonce, not a wildcard. A list of exact digests, and anything whose bytes don't match one of them does not run.&lt;/p&gt;
&lt;p&gt;That is the strong version of the policy, and it is strong for a boring reason: a hash is a statement about content that nobody can forge. If an injection lands in my HTML, it does not matter that it sits in a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag on my own origin. Its hash isn't on the list, so it's inert.&lt;/p&gt;
&lt;p&gt;The cost of that arrived the first time I put a fenced code block in an article.&lt;/p&gt;
&lt;h3 id=&quot;what-a-hash-can-and-cannot-cover&quot;&gt;What a hash can and cannot cover&lt;/h3&gt;
&lt;p&gt;Astro computes these hashes during the build and writes them into a &lt;code&gt;&amp;lt;meta http-equiv&amp;gt;&lt;/code&gt;. It knows the bytes of every script it bundles, so it can digest them and emit a policy that matches.&lt;/p&gt;
&lt;p&gt;The limit is in that sentence. It hashes what it bundles. Anything that produces markup at render time, after the policy has been written, is invisible to it.&lt;/p&gt;
&lt;p&gt;There is exactly one script on this site that Astro doesn't bundle, and it earns its exception. The anti-FOUC snippet that reads the stored theme has to run before first paint, so it is &lt;code&gt;is:inline&lt;/code&gt; and Astro leaves it alone. Its hash is maintained by hand:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;scriptDirective: {
  resources: [&quot;'self'&quot;, 'https://challenges.cloudflare.com'],
  hashes: ['sha256-WV81hIAeXjEdgj/cFIXtOf53g8pIquCjmXQuCHOehlw='],
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which is fine for one script that changes twice a year. It stops being fine as a general strategy the moment something starts generating markup on every page.&lt;/p&gt;
&lt;h3 id=&quot;shiki-was-the-first-casualty&quot;&gt;Shiki was the first casualty&lt;/h3&gt;
&lt;p&gt;Astro ships Shiki for Markdown syntax highlighting and turns it on by default. It is genuinely good. It also colours code by wrapping every token in a &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; with an inline &lt;code&gt;style&lt;/code&gt; attribute.&lt;/p&gt;
&lt;p&gt;Under &lt;code&gt;style-src 'self'&lt;/code&gt; with no &lt;code&gt;unsafe-inline&lt;/code&gt;, every one of those attributes is blocked. Code blocks render as undifferentiated grey text.&lt;/p&gt;
&lt;p&gt;The obvious repair is to allow them, and the way you allow a &lt;code&gt;style=&lt;/code&gt; attribute specifically is &lt;code&gt;'unsafe-hashes'&lt;/code&gt;. I read the spec on that keyword twice, because the name is doing a lot of honest work. It permits hashed content in attribute position, and attribute position is where a large share of real-world injection lands. Adding it to make code look nice would mean weakening the exact property I built the policy for, on behalf of a feature nobody asked me for.&lt;/p&gt;
&lt;p&gt;So I turned it off:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;markdown: {
  syntaxHighlight: false,
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Code blocks now emit plain &lt;code&gt;&amp;lt;pre&amp;gt;&amp;lt;code&amp;gt;&lt;/code&gt;, which the policy has no objection to. The colour lives in a stylesheet, keyed off classes, served from &lt;code&gt;'self'&lt;/code&gt;, and no hash is involved because it is not inline anything.&lt;/p&gt;
&lt;p&gt;I want to be precise about what I gave up, because &quot;just disable it&quot; is a suspiciously comfortable ending. I lost per-token semantic colour. What I have is monospace with sensible contrast, and I decided that a code block being legible matters more than a keyword being purple. If a piece ever genuinely needs highlighting, the way in is a build-time transform that emits classes instead of styles, not a relaxed policy.&lt;/p&gt;
&lt;h3 id=&quot;the-rule-that-fell-out-of-it&quot;&gt;The rule that fell out of it&lt;/h3&gt;
&lt;p&gt;The interesting part wasn't the config flag. It was noticing that Shiki is not a special case, it is the first instance of a class, and the class is &quot;anything that writes style into an attribute&quot;.&lt;/p&gt;
&lt;p&gt;My own code does this constantly if I let it. A one-line margin tweak in a component is easier to write as &lt;code&gt;style=&quot;margin-left:8px&quot;&lt;/code&gt; than as a class and a rule in a stylesheet. It also passes the build, looks correct in &lt;code&gt;astro preview&lt;/code&gt;, and dies silently in production, because preview does not serve the real policy.&lt;/p&gt;
&lt;p&gt;So that stopped being allowed, and the ban is written where the temptation is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* Qui e non come style= inline: un attributo style richiederebbe 'unsafe-hashes' nella CSP. */
#copy-email-btn{ margin-left:8px; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Eight pixels of margin, with a comment explaining why it lives in a stylesheet. It looks like over-documentation until you picture the version of me who is in a hurry, sees a naked eight-pixel rule with no explanation, and decides it would read better inline.&lt;/p&gt;
&lt;h3 id=&quot;the-test-is-on-the-built-output-not-the-source&quot;&gt;The test is on the built output, not the source&lt;/h3&gt;
&lt;p&gt;None of that survives on discipline. It survives because a test reads the built HTML and fails on anything it finds.&lt;/p&gt;
&lt;p&gt;For every page in &lt;code&gt;dist/&lt;/code&gt;, it extracts the inline scripts, hashes each one, and asserts the digest appears in the policy. When it fails it hands you the fix:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Script inline senza hash nella CSP.
Aggiungi 'sha256-…' a security.csp.scriptDirective.hashes in astro.config.mjs.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then two assertions for the class Shiki introduced me to. No &lt;code&gt;style=&lt;/code&gt; attribute anywhere. No inline &lt;code&gt;on…=&lt;/code&gt; handler anywhere. Both name the same reason in their failure message, that allowing them would require &lt;code&gt;'unsafe-hashes'&lt;/code&gt;, so whoever hits it learns the policy instead of just learning that CI is angry.&lt;/p&gt;
&lt;p&gt;A fourth one guards a different failure. My real policy lives in the meta tag, and a CSP in the headers would be applied as an intersection with it, so a plain &lt;code&gt;script-src 'self'&lt;/code&gt; written there would cancel every hash and take the site offline. That test allows exactly one directive in the headers file, &lt;code&gt;frame-ancestors&lt;/code&gt;, which is ignored inside a meta tag by specification and therefore has to live there.&lt;/p&gt;
&lt;p&gt;Two more of them exist only to keep the suite honest. One asserts the build produced pages at all. The other asserts that somewhere in &lt;code&gt;dist/&lt;/code&gt; at least one inline script was found, because the day my regex stops matching, every per-page assertion starts passing on an empty list and the whole file goes green while checking nothing. A test that cannot fail is not a test, and the cheapest way to catch one is to assert that its input is non-empty.&lt;/p&gt;
&lt;p&gt;There is a trap in this design worth naming. The tests read &lt;code&gt;dist/&lt;/code&gt;, so running them without rebuilding grades yesterday's output. That is why the script is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;test&quot;: &quot;npm run build &amp;amp;&amp;amp; npm run test:csp&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I have watched that suite pass on a stale build. It is very reassuring and completely meaningless.&lt;/p&gt;
&lt;h3 id=&quot;worth-checking-on-yours&quot;&gt;Worth checking on yours&lt;/h3&gt;
&lt;p&gt;If you have a CSP, load your site and open the console rather than reading the policy. Blocked resources report themselves there, and a policy that is silently breaking one widget on one page looks identical to a working one from the config file.&lt;/p&gt;
&lt;p&gt;If you are on &lt;code&gt;unsafe-inline&lt;/code&gt; today, the useful question isn't how to reach a hash policy in one move. It's which dependency would break first if you did. For me it was the highlighter shipped by default in my own framework, and I would not have guessed that before it happened.&lt;/p&gt;
&lt;p&gt;And if you already run a strict policy, go find where the exception is. There usually is one, it usually has a good reason, and the good reason is usually two years old.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32699/a-hash-based-csp-has-no-room-for-a-syntax-highlighter</guid>
<pubDate>Tue, 27 Oct 2026 16:19:34 +0000</pubDate>
</item>
<item>
<title>Diagnose scroll snap failures: prerequisites, decision, result, limitation</title>
<link>https://coderlegion.com/32433/diagnose-scroll-snap-failures-prerequisites-decision-result-limitation</link>
<description>&lt;p&gt;Prerequisites&lt;/p&gt;
&lt;p&gt;You should be comfortable with CSS basics (overflow, writing modes) and know where to edit the scrolling container and its children in your stylesheet. Familiarity with scroll-snap-type, scroll-snap-align, and the difference between physical (x/y) and logical (inline/block) axes is helpful.&lt;/p&gt;
&lt;p&gt;Implementation decision&lt;/p&gt;
&lt;p&gt;The diagnostic approach checks four things in order: container opt-in (scroll-snap-type), child snap alignments (scroll-snap-align), axis correspondence with actual scroll motion, and strictness (proximity vs mandatory). The helper returns a small report listing issues and concrete fixes so you can change only the declaration that matters.&lt;/p&gt;
&lt;p&gt;Expected result&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the container never had scroll-snap-type, the helper tells you to add it.&lt;/li&gt;
&lt;li&gt;If children lack scroll-snap-align, it tells you to add alignment to the snapping items.&lt;/li&gt;
&lt;li&gt;If the axis does not match the motion, it suggests changing the container axis to x/y/inline/block.&lt;/li&gt;
&lt;li&gt;If strictness is proximity, it suggests mandatory when you require guaranteed landings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One limitation&lt;/p&gt;
&lt;p&gt;This tool inspects declarations and relationships; it does not simulate the browser’s snapping physics. Logical axis changes, multi-value child alignments, and subtle input momentum can still cause behavior that the heuristic can’t predict. Use it to find the likely declaration to edit, then verify in-browser across viewports and input types.&lt;/p&gt;
&lt;p&gt;The server will inject the verified implementation below as a fenced example.&lt;/p&gt;
&lt;h3 id=&quot;working-example&quot;&gt;Working example&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export function diagnoseSnap(containerStyles = {}, childrenStyles = []) {
  const result = { ok: false, issues: [], fixes: [] };

  const rawType = String(containerStyles.scrollSnapType ?? 'none').trim();
  if (!rawType || rawType === 'none') {
    result.issues.push('container-not-opted-in');
    result.fixes.push('Add scroll-snap-type on the scroll container, for example: &quot;scroll-snap-type: x mandatory&quot;.');
    return result;
  }

  const [axisToken = 'none', strictnessToken] = rawType.split(/\s+/);
  const axis = axisToken;
  const strictness = strictnessToken ?? 'proximity';

  const parsedChildren = childrenStyles
    .map((child) =&amp;gt; {
      const rawAlign = String(child?.scrollSnapAlign ?? '').trim();
      if (!rawAlign) return null;
      const parts = rawAlign.split(/\s+/).filter(Boolean);
      return parts.length === 1
        ? { rawAlign, parts, block: parts[0], inline: parts[0] }
        : { rawAlign, parts, block: parts[0], inline: parts[1] };
    })
    .filter(Boolean);

  if (parsedChildren.length === 0) {
    result.issues.push('missing-snap-align');
    result.fixes.push('Add scroll-snap-align on the snapping children, for example: &quot;scroll-snap-align: center;&quot;.');
  }

  const hasAxisMatch = parsedChildren.some((child) =&amp;gt; {
    if (axis === 'both') return child.block !== 'auto' || child.inline !== 'auto';
    if (axis === 'x' || axis === 'inline') return child.parts.length === 1 &amp;amp;&amp;amp; ['start', 'center', 'end'].includes(child.inline);
    if (axis === 'y' || axis === 'block') return ['start', 'center', 'end'].includes(child.block);
    return false;
  });

  if (parsedChildren.length &amp;gt; 0 &amp;amp;&amp;amp; !hasAxisMatch) {
    result.issues.push('axis-mismatch');
    result.fixes.push(`Change the container axis to match the movement you want; the current value &quot;${axis}&quot; does not line up with the child alignments.`);
  }

  if (strictness === 'proximity') {
    result.issues.push('proximity-strictness');
    result.fixes.push('If you need a guaranteed landing, use &quot;mandatory&quot; instead of &quot;proximity&quot;.');
  }

  if (result.issues.length === 0) {
    result.ok = true;
    result.message = 'snapping-should-work';
  }

  return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read the full article: &lt;a rel=&quot;nofollow&quot; href=&quot;https://chriseugenerodriguez.com/blog/scroll-snap-axis-align-strictness-diagnose&quot;&gt;https://chriseugenerodriguez.com/blog/scroll-snap-axis-align-strictness-diagnose&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32433/diagnose-scroll-snap-failures-prerequisites-decision-result-limitation</guid>
<pubDate>Sun, 25 Oct 2026 22:09:27 +0000</pubDate>
</item>
<item>
<title>Your Flex Child Won't Shrink, and overflow: hidden Isn't the Fix (It's min-width: 0)&quot;</title>
<link>https://coderlegion.com/32086/your-flex-child-wont-shrink-and-overflow-hidden-isnt-the-fix-its-min-width-0</link>
<description>&lt;p&gt;You build a dashboard. Sidebar on the left, content on the right, a table or a code block inside. It looks perfect with your sample data.&lt;/p&gt;
&lt;p&gt;Then a user pastes a long URL into a field, and the whole page grows a horizontal scrollbar. The sidebar gets squeezed, the layout breaks, and you start adding &lt;code&gt;overflow: hidden&lt;/code&gt; to everything until the symptoms go away.&lt;/p&gt;
&lt;p&gt;There is a one-line fix, and it is not &lt;code&gt;overflow: hidden&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-layout-that-breaks&quot;&gt;The Layout That Breaks&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&quot;layout&quot;&amp;gt;
  &amp;lt;aside class=&quot;sidebar&quot;&amp;gt;Menu&amp;lt;/aside&amp;gt;
  &amp;lt;main class=&quot;content&quot;&amp;gt;
    &amp;lt;pre&amp;gt;https://example.com/a-very-long-url-with-no-break-points-in-it-at-all-1234567890&amp;lt;/pre&amp;gt;
  &amp;lt;/main&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.layout {
  display: flex;
}
.sidebar {
  width: 240px;
  flex-shrink: 0;
}
.content {
  flex: 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.content&lt;/code&gt; is told to take the remaining space with &lt;code&gt;flex: 1&lt;/code&gt;. Nobody told it to stay &lt;em&gt;inside&lt;/em&gt; that space, and that is the entire problem.&lt;/p&gt;
&lt;h3 id=&quot;why-it-happens&quot;&gt;Why It Happens&lt;/h3&gt;
&lt;p&gt;Flex items and grid items have a default of &lt;code&gt;min-width: auto&lt;/code&gt;. For a flex item, &lt;code&gt;auto&lt;/code&gt; means:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Do not shrink narrower than the smallest size your content can be.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For a paragraph of normal words, the smallest size is the longest single word, because the line can wrap between words. For a long URL, a &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; block, a wide table or an image, there is nowhere to wrap, so the smallest size is the &lt;strong&gt;full width of the content&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;So the browser is not ignoring your layout. It is obeying a rule you never knew existed: the item refuses to shrink below its content, and the overflow spills into the page.&lt;/p&gt;
&lt;h3 id=&quot;why-overflow-hidden-looks-like-a-fix-sometimes-&quot;&gt;Why &lt;code&gt;overflow: hidden&lt;/code&gt; Looks Like a Fix (Sometimes)&lt;/h3&gt;
&lt;p&gt;Setting &lt;code&gt;overflow&lt;/code&gt; to anything other than &lt;code&gt;visible&lt;/code&gt; changes how &lt;code&gt;min-width: auto&lt;/code&gt; is calculated. The automatic minimum becomes 0 for that item. That is why &lt;code&gt;overflow: hidden&lt;/code&gt; on the right element makes the bug vanish.&lt;/p&gt;
&lt;p&gt;It works by accident, and it has a cost: it clips content, can cut off focus outlines and dropdowns, and does nothing useful if you put it on the wrong element. Fixing the cause is cleaner.&lt;/p&gt;
&lt;h3 id=&quot;fix-1-min-width-0-on-the-flex-child&quot;&gt;Fix 1: min-width: 0 on the Flex Child&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.content {
  flex: 1;
  min-width: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You are telling the item that it is allowed to shrink below its content. Now &lt;code&gt;.content&lt;/code&gt; stays inside its space, and the long URL can be scrolled, wrapped or cut off by something you control:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.content pre {
  overflow-x: auto;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Tailwind, the utility is &lt;code&gt;min-w-0&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;main class=&quot;flex-1 min-w-0&quot;&amp;gt;
  &amp;lt;pre class=&quot;overflow-x-auto&quot;&amp;gt;...&amp;lt;/pre&amp;gt;
&amp;lt;/main&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;fix-2-grid-s-1fr-has-the-same-trap&quot;&gt;Fix 2: Grid's 1fr Has the Same Trap&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.layout {
  display: grid;
  grid-template-columns: 240px 1fr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;1fr&lt;/code&gt; is not &quot;one equal share, and no more&quot;. It is shorthand for &lt;code&gt;minmax(auto, 1fr)&lt;/code&gt;, so the column cannot get narrower than its content. The same long URL stretches the column and the page.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.layout {
  display: grid;
  grid-template-columns: 240px minmax(0, 1fr);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;minmax(0, 1fr)&lt;/code&gt; allows the column to shrink to nothing if it has to. A useful side note for Tailwind users: &lt;code&gt;grid-cols-2&lt;/code&gt; and its siblings already compile to &lt;code&gt;repeat(2, minmax(0, 1fr))&lt;/code&gt;, which is why many people never meet this bug in the grid, and then meet it as soon as they write a custom template column.&lt;/p&gt;
&lt;h3 id=&quot;fix-3-truncating-text-with-an-ellipsis-inside-flex&quot;&gt;Fix 3: Truncating Text With an Ellipsis Inside Flex&lt;/h3&gt;
&lt;p&gt;This one catches almost everybody:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.row {
  display: flex;
  gap: 8px;
}
.title {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It does not truncate, because the flex item sizes itself to the text. Add &lt;code&gt;min-width: 0&lt;/code&gt; to &lt;code&gt;.title&lt;/code&gt; and the ellipsis appears. In Tailwind, &lt;code&gt;truncate min-w-0&lt;/code&gt; on the same element does it.&lt;/p&gt;
&lt;h3 id=&quot;fix-4-long-words-need-the-right-overflow-wrap&quot;&gt;Fix 4: Long Words Need the Right overflow-wrap&lt;/h3&gt;
&lt;p&gt;For user-generated text such as comments, names and pasted links, allow breaking inside a word:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.comment {
  overflow-wrap: anywhere;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;overflow-wrap: break-word&lt;/code&gt; looks equivalent, but it does not change the item's minimum size calculation, so inside a flex or grid item it can still leave the layout stretched. &lt;code&gt;anywhere&lt;/code&gt; does take the break points into account, and the item can shrink.&lt;/p&gt;
&lt;h3 id=&quot;which-one-when&quot;&gt;Which One When&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Symptom &lt;/th&gt;&lt;th&gt;Fix&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Flex child pushes the page wider &lt;/td&gt;&lt;td&gt;&lt;code&gt;min-width: 0&lt;/code&gt; on that child&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Grid column grows past its space &lt;/td&gt;&lt;td&gt;&lt;code&gt;minmax(0, 1fr)&lt;/code&gt; instead of &lt;code&gt;1fr&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Ellipsis not appearing in a flex row &lt;/td&gt;&lt;td&gt;&lt;code&gt;min-width: 0&lt;/code&gt; plus the truncation rules&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Long pasted word or URL &lt;/td&gt;&lt;td&gt;&lt;code&gt;overflow-wrap: anywhere&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Wide table or code block &lt;/td&gt;&lt;td&gt;Own scroll container with &lt;code&gt;overflow-x: auto&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;the-rule&quot;&gt;The Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Flex and grid items refuse to shrink below their content by default. If something inside a layout can be wider than the space it is given, tell the container it is allowed to shrink with &lt;code&gt;min-width: 0&lt;/code&gt; or &lt;code&gt;minmax(0, 1fr)&lt;/code&gt;, and decide where the overflow should scroll or wrap.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Quick audit: open your app and paste a 200-character string with no spaces into every input that is later displayed. Then widen a table to 40 columns. If the page grows a horizontal scrollbar, you have found a missing &lt;code&gt;min-width: 0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I test every layout in my &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com&quot;&gt;Next.js and Tailwind templates&lt;/a&gt; against this kind of hostile content, because real users do paste long strings, and a dashboard has to survive it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What is the ugliest layout bug a single piece of user content has caused you? Drop it in the comments.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32086/your-flex-child-wont-shrink-and-overflow-hidden-isnt-the-fix-its-min-width-0</guid>
<pubDate>Thu, 22 Oct 2026 15:08:46 +0000</pubDate>
</item>
<item>
<title>min(), max(), and clamp() Will Replace 80% of Your Media Queries</title>
<link>https://coderlegion.com/30739/min-max-and-clamp-will-replace-80-of-your-media-queries</link>
<description>&lt;p&gt;Writing dozens of @media (min-width: ...) breakpoints creates rigid, fragmented CSS stylesheets. Modern CSS handles dynamic fluid typography and fluid spacing natively without media queries.&lt;/p&gt;
&lt;p&gt;CSS&lt;br&gt;
/&lt;em&gt; Old Way: Rigid breakpoint hopping &lt;/em&gt;/&lt;br&gt;
h1 { font-size: 1.75rem; }&lt;br&gt;
@media (min-width: 768px) { h1 { font-size: 2.5rem; } }&lt;br&gt;
@media (min-width: 1200px) { h1 { font-size: 3.5rem; } }&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Modern Way: Fluid typography in 1 line &lt;/em&gt;/&lt;br&gt;
h1 {&lt;br&gt;
  font-size: clamp(1.75rem, 4vw + 1rem, 3.5rem);&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Fluid layout container with max boundary and dynamic padding &lt;/em&gt;/&lt;br&gt;
.container {&lt;br&gt;
  width: min(100% - 2rem, 1200px);&lt;br&gt;
  margin-inline: auto;&lt;br&gt;
}&lt;br&gt;
Why it changes your career: Fluid layout primitives produce far cleaner, more resilient user interfaces that adapt smoothly across any viewport without brittle media query hacks.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30739/min-max-and-clamp-will-replace-80-of-your-media-queries</guid>
<pubDate>Thu, 22 Oct 2026 06:52:00 +0000</pubDate>
</item>
<item>
<title>FSCSS layout helpers: stack, hstack, center, size, rounded, ratio-fit(), etc</title>
<link>https://coderlegion.com/31947/fscss-layout-helpers-stack-hstack-center-size-rounded-ratio-fit-etc</link>
<description>&lt;p&gt;&lt;strong&gt;FSCSS layout helpers: &lt;code&gt;stack&lt;/code&gt;, &lt;code&gt;hstack&lt;/code&gt;, &lt;code&gt;center&lt;/code&gt;, &lt;code&gt;size&lt;/code&gt;, &lt;code&gt;rounded&lt;/code&gt;, &lt;code&gt;ratio-fit()&lt;/code&gt;, and friends&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;From version 1.2.5 FSCSS added a set of &lt;strong&gt;property shorthands&lt;/strong&gt; aimed at everyday layout and chrome—spacing axes, flex stacks, sizing, and aspect boxes—without inventing a new component system. They compile to ordinary CSS. Use them in &lt;code&gt;.fscss&lt;/code&gt; (CLI, runtime, or &lt;a rel=&quot;nofollow&quot; href=&quot;https://fscss.devtem.org/ide&quot;&gt;IDE&lt;/a&gt;); production can ship the compiled sheet only.&lt;/p&gt;
&lt;p&gt;Full list and expansions: &lt;a rel=&quot;nofollow&quot; href=&quot;https://fscss.devtem.org/shorthands&quot;&gt;fscss.devtem.org/shorthands&lt;/a&gt; · language: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/fscss&quot;&gt;fscss@1.2.5 - 1.2.6+&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;why-these-exist&quot;&gt;Why these exist&lt;/h4&gt;
&lt;p&gt;A lot of UI CSS is the same four ideas:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Space &lt;strong&gt;along one axis&lt;/strong&gt; (padding/margin x or y)  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stack&lt;/strong&gt; children (column or row + gap)  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Center&lt;/strong&gt; content in a box  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Size&lt;/strong&gt; a box (square, both axes, or fit a ratio)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Native CSS can do all of that, but the typing is noisy (&lt;code&gt;display: flex; flex-direction: column; gap: …&lt;/code&gt; every time). These helpers expand at compile time so the mental model stays short.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;spacing-axes&quot;&gt;Spacing axes&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Shorthand &lt;/th&gt;&lt;th&gt;Idea &lt;/th&gt;&lt;th&gt;Typical expansion&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;mx&lt;/code&gt; / &lt;code&gt;my&lt;/code&gt; &lt;/td&gt;&lt;td&gt;margin on x / y &lt;/td&gt;&lt;td&gt;&lt;code&gt;margin-left&lt;/code&gt; + &lt;code&gt;margin-right&lt;/code&gt; / top + bottom&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;px&lt;/code&gt; / &lt;code&gt;py&lt;/code&gt; &lt;/td&gt;&lt;td&gt;padding on x / y &lt;/td&gt;&lt;td&gt;same pattern for padding&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;inset-x&lt;/code&gt; / &lt;code&gt;inset-y&lt;/code&gt; &lt;/td&gt;&lt;td&gt;positioned inset on one axis &lt;/td&gt;&lt;td&gt;without forcing the other axis to &lt;code&gt;0&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.card {
  px: 20px;
  py: 16px;
  mx: auto;
  max-w: 420px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use these when you only care about one axis. Prefer full &lt;code&gt;margin&lt;/code&gt; / &lt;code&gt;padding&lt;/code&gt; when all four sides share one value.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;stack-and-hstack&quot;&gt;&lt;code&gt;stack&lt;/code&gt; and &lt;code&gt;hstack&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Vertical and horizontal flex stacks with gap:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.panel {
  stack: 12px;   /* column + gap */
}

.toolbar {
  hstack: 8px;   /* row + gap */
  fwrap: wrap;   /* if you also use wrap helpers */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Roughly: &lt;code&gt;display: flex&lt;/code&gt;, direction column or row, and &lt;code&gt;gap&lt;/code&gt; from the value. Ideal for forms, toolbars, and card bodies where “children in a line with space between them” is the whole layout story.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;center&quot;&gt;&lt;code&gt;center&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Center children in the box (axis or both, depending on the form your build supports—commonly flex centering):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.hero {
  min-h: 40vh;
  center: both; /* or center: true — see docs */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Handy for empty states, icon wells, and hero blocks without repeating &lt;code&gt;align-items&lt;/code&gt; / &lt;code&gt;justify-content&lt;/code&gt; every time.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;size-and-friends&quot;&gt;&lt;code&gt;size&lt;/code&gt; and friends&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Helper &lt;/th&gt;&lt;th&gt;Role&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;size&lt;/code&gt; &lt;/td&gt;&lt;td&gt;width &lt;strong&gt;and&lt;/strong&gt; height (one value → square, or two values)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;max-size&lt;/code&gt; / &lt;code&gt;min-size&lt;/code&gt; &lt;/td&gt;&lt;td&gt;both axes’ max/min&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;insert: ratio-fit();&lt;/code&gt; &lt;/td&gt;&lt;td&gt;size a box to a ratio inside a container&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.avatar {
  size: 48px;
  rounded: 999px;
}

.thumb {
  size: 100% 180px;
}

.media {
  inset: ratio-fit(16 / 9);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ratio-fit&lt;/code&gt; is for the common “keep 16:9 (or any ratio) without a padding-hack essay.” Exact expansion is in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://fscss.devtem.org/shorthands&quot;&gt;shorthands reference&lt;/a&gt;—treat it as the declarative cousin of aspect-ratio + constrained width/height patterns.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;rounded-and-surface-chrome&quot;&gt;&lt;code&gt;rounded&lt;/code&gt; and surface chrome&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.card {
  rounded: 16px;
  bg: #121a30;
  shadow: 0 10px 30px #0003;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rounded&lt;/code&gt; maps to &lt;code&gt;border-radius&lt;/code&gt;. Together with color/spacing shorthands (&lt;code&gt;bg&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt;, &lt;code&gt;m&lt;/code&gt;, …) you can sketch a card shell in a few lines and still compile to standard properties.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;small-composed-example&quot;&gt;Small composed example&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.page {
  min-h: 100vh;
  p: 24px;
  bg: #0b1020;
  color: #e6edf7;
  ff: system-ui, sans-serif;
}

.wrap {
  max-w: 960px;
  mx: auto;
  stack: 20px;
}

.row {
  hstack: 16px;
  fwrap: wrap;
}

.card {
  flex: 1;
  min-w: 240px;
  p: 20px;
  rounded: 16px;
  bg: #121a30;
  border: 1px solid #22304f;
  stack: 12px;
}

.icon {
  size: 40px;
  rounded: 12px;
  center: true;
  bg: #1b2745;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read it top to bottom: page padding → centered column → horizontal row of cards → each card is a vertical stack. No design-system runtime—only CSS the browser already understands after compile.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;runtime-note-fscss-props&quot;&gt;Runtime note: &lt;code&gt;fscss-props&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;If you only need &lt;strong&gt;property&lt;/strong&gt; shorthands in the browser (not full &lt;code&gt;@arr&lt;/code&gt; / &lt;code&gt;@define&lt;/code&gt; / &lt;code&gt;pattern()&lt;/code&gt;), &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Figsh/fscss-props&quot;&gt;fscss-props&lt;/a&gt; expands a similar surface at runtime and exposes JS APIs (&lt;code&gt;element.fscssProp&lt;/code&gt;, &lt;code&gt;fscssPropText&lt;/code&gt;) for live updates. Full language features still go through the FSCSS compiler (CLI, &lt;code&gt;runtime.js&lt;/code&gt; / &lt;code&gt;esm.js&lt;/code&gt;, or the IDE).&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;when-to-use-what&quot;&gt;When to use what&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Goal &lt;/th&gt;&lt;th&gt;Prefer&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;One-off page / prototype &lt;/td&gt;&lt;td&gt;Shorthands in &lt;code&gt;.fscss&lt;/code&gt; + CDN runtime or IDE&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Design tokens + charts + modules &lt;/td&gt;&lt;td&gt;&lt;code&gt;@import&lt;/code&gt; packs (&lt;code&gt;st-core@v2&lt;/code&gt;, etc.) &lt;strong&gt;and&lt;/strong&gt; shorthands for local layout&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Production &lt;/td&gt;&lt;td&gt;Compile with CLI; ship the CSS file&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;JS-driven tweaks of padding/size only &lt;/td&gt;&lt;td&gt;&lt;code&gt;fscss-props&lt;/code&gt; or set CSS variables / longhands from JS&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Shorthands do not replace modules. They remove friction &lt;strong&gt;between&lt;/strong&gt; modules—nav bars, card grids, toolbars—where flex + gap + radius is enough.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;try-it&quot;&gt;Try it&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install fscss@latest 
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/fscss@1.2.6/runtime.min.js&quot; defer&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or open the &lt;a rel=&quot;nofollow&quot; href=&quot;https://fscss.devtem.org/ide&quot;&gt;FSCSS IDE&lt;/a&gt;, paste a &lt;code&gt;.card { stack: 12px; rounded: 16px; p: 20px; }&lt;/code&gt; block, and hit compile.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/31947/fscss-layout-helpers-stack-hstack-center-size-rounded-ratio-fit-etc</guid>
<pubDate>Wed, 21 Oct 2026 09:20:38 +0000</pubDate>
</item>
<item>
<title>A Declarative Naming Layer for the View Transition API</title>
<link>https://coderlegion.com/30964/a-declarative-naming-layer-for-the-view-transition-api</link>
<description>&lt;p&gt;&lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/2FyH32I0Lyk?si=SitUWu1N-Ueyuw7P&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;allowfullscreen&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;p&gt;For a long time, I treated interface animation as mostly decorative. It could make a product feel more polished or visually impressive, but I rarely considered it functionally important. In fact, I usually preferred to disable animations wherever possible. A dropdown did not seem to need an animation. Smooth scrolling often felt slower than an immediate jump. Waiting for UI motion to finish could feel like unnecessary latency.&lt;/p&gt;
&lt;p&gt;Working on more complex interfaces changed my view.&lt;/p&gt;
&lt;p&gt;Transitions can be a form of user feedback. When an interaction causes a substantial change in the interface, the developer already knows exactly what happened: which component disappeared, which one replaced it, which panel expanded, and where the new state came from. A user who is still learning the interface does not have that internal model.&lt;/p&gt;
&lt;p&gt;An abrupt state change can therefore be difficult to interpret. A transition can preserve visual continuity between the old and new states. When an element moves, expands, collapses, or transforms into another part of the interface, the motion gives the user additional information about the relationship between those states. It helps explain what their action changed and how the new layout relates to the previous one.&lt;/p&gt;
&lt;p&gt;That makes transitions part of UX rather than merely visual styling. They can reduce the cognitive cost of navigating an unfamiliar interface and make its behavior easier to learn.&lt;/p&gt;
&lt;p&gt;This is one of the reasons I became interested in the View Transition API. It gives the browser a native mechanism for creating complex transitions between DOM states without manually implementing every animation and coordinating the old and new layouts yourself. For me, this changed View Transitions from a visual extra into a useful part of interface architecture.&lt;/p&gt;
&lt;p&gt;Once I started using them more extensively, however, a different problem appeared: managing &lt;code&gt;view-transition-name&lt;/code&gt; across a large component hierarchy quickly became verbose and difficult to maintain.&lt;/p&gt;
&lt;p&gt;That is the problem this article is about.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The View Transition API is one of the browser APIs I enjoy using most. For simple cases, it is remarkably compact: update the DOM inside &lt;code&gt;document.startViewTransition()&lt;/code&gt;, assign a few &lt;code&gt;view-transition-name&lt;/code&gt; values, and let the browser handle the snapshots and interpolation.&lt;/p&gt;
&lt;p&gt;The complexity changes quickly once the interface contains many independently changing elements.&lt;/p&gt;
&lt;p&gt;In a real application, the same element may participate in different transitions depending on the operation. A transition may involve elements distributed across several Vue components. Some elements should inherit a transition namespace from their ancestors, while others should start a new namespace or stop propagation. Multiple independent transition hierarchies may coexist in the same DOM tree.&lt;/p&gt;
&lt;p&gt;I initially handled these cases individually, including with coding agents. The individual implementations worked, but the amount of imperative orchestration kept growing: find elements, assign names, coordinate old and new DOM states, wait for Vue updates, remove temporary state, handle dynamic instances, and repeat similar logic for the next transition.&lt;/p&gt;
&lt;p&gt;The code was becoming substantially more verbose than the UI behavior it described.&lt;/p&gt;
&lt;p&gt;I eventually moved the problem into a separate declarative naming layer.&lt;/p&gt;
&lt;p&gt;The result is a small internal system used in &lt;a rel=&quot;nofollow&quot; href=&quot;https://aleklabs.dev/eye-trainer&quot;&gt;AlekLabs Eye Trainer&lt;/a&gt;. View transition participation is described directly in component templates using attributes, while a runtime function activates the required transition hierarchy only for the duration of a transition. Vite compiler transforms, generated TypeScript types, and a VS Code grammar provide the tooling around it.&lt;/p&gt;
&lt;p&gt;The browser still performs the actual View Transition. The new layer deals with naming and orchestration.&lt;/p&gt;
&lt;h3 id=&quot;moving-transition-identity-out-of-css&quot;&gt;Moving Transition Identity Out of CSS&lt;/h3&gt;
&lt;p&gt;A normal View Transition implementation often starts with CSS such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.card {
    view-transition-name: card;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That works well while an element has one permanent transition identity.&lt;/p&gt;
&lt;p&gt;My application needed something more dynamic. A DOM element can participate in one transition when a panel changes, another when a dialog opens, and a more specific child transition when only part of the same UI changes.&lt;/p&gt;
&lt;p&gt;Keeping all of that identity in CSS made the relationship between the component structure and transition structure increasingly difficult to see.&lt;/p&gt;
&lt;p&gt;I wanted the template itself to answer a simple question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Which View Transition hierarchies can this element participate in?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;CSS could then remain responsible for the visual behavior of the generated View Transition pseudo-elements.&lt;/p&gt;
&lt;p&gt;The distinction became important. The attributes in my templates do not directly set &lt;code&gt;view-transition-name&lt;/code&gt;. They are persistent metadata describing possible transition relationships. The actual &lt;code&gt;view-transition-name&lt;/code&gt; values exist only while a particular transition is running.&lt;/p&gt;
&lt;h3 id=&quot;first-attempt-vue-directives&quot;&gt;First Attempt: Vue Directives&lt;/h3&gt;
&lt;p&gt;Vue directives initially looked like a natural solution.&lt;/p&gt;
&lt;p&gt;Something like this would have been expressive:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;al-panel v-transition=&quot;'settings'&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The limitation appeared as soon as the directive was placed on components.&lt;/p&gt;
&lt;p&gt;Vue directives used on components are applied to the component root. Unlike fallthrough attributes, they cannot be forwarded to another element with &lt;code&gt;v-bind=&quot;$attrs&quot;&lt;/code&gt;. Multi-root components add further restrictions.&lt;/p&gt;
&lt;p&gt;My UI has a deep component hierarchy, including many internal UI components. I needed transition metadata to move naturally through those abstraction layers.&lt;/p&gt;
&lt;p&gt;Ordinary attributes already have exactly this property in Vue.&lt;/p&gt;
&lt;p&gt;That made attributes a much better transport mechanism.&lt;/p&gt;
&lt;h3 id=&quot;a-small-attribute-language&quot;&gt;A Small Attribute Language&lt;/h3&gt;
&lt;p&gt;The initial attribute names were too verbose. Repeating a long &lt;code&gt;view-transition-*&lt;/code&gt; or application-specific prefix throughout large templates made the transition structure visually noisy.&lt;/p&gt;
&lt;p&gt;I introduced &lt;code&gt;$&lt;/code&gt; as a source-level marker for View Transition metadata.&lt;/p&gt;
&lt;p&gt;A simplified example looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;al-panel-expandable :$modulator-panel=&quot;num&quot;&amp;gt;
    &amp;lt;modulation-curve-setting $-motion /&amp;gt;
    &amp;lt;al-adjust $-amount /&amp;gt;
&amp;lt;/al-panel-expandable&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first attribute establishes a transition family. If &lt;code&gt;num&lt;/code&gt; is &lt;code&gt;2&lt;/code&gt;, its resolved name can become:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;modulator-panel-2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The child attributes extend the inherited family:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;modulator-panel-2-motion
modulator-panel-2-amount
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The interesting part is that &lt;code&gt;$-motion&lt;/code&gt; contains no knowledge of &lt;code&gt;modulator-panel&lt;/code&gt;, the instance number, or the component in which the hierarchy started.&lt;/p&gt;
&lt;p&gt;It simply declares its position inside the current transition family.&lt;/p&gt;
&lt;p&gt;This makes transition structure composable across components.&lt;/p&gt;
&lt;h3 id=&quot;hierarchical-names&quot;&gt;Hierarchical Names&lt;/h3&gt;
&lt;p&gt;The naming layer behaves as a hierarchy rather than a flat collection of unrelated IDs.&lt;/p&gt;
&lt;p&gt;A direct attribute starts a family:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;al-panel :$al-setting=&quot;targetId&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A descendant can extend it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;al-slider $-value /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a target ID of &lt;code&gt;rotation&lt;/code&gt;, the runtime can resolve this as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;al-setting-rotation
al-setting-rotation-value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hierarchy can cross Vue component boundaries because the compiler output consists of normal attributes.&lt;/p&gt;
&lt;p&gt;Several transition families can also coexist in the same area of the application. An element can permanently declare metadata for several possible scenarios while receiving only the actual &lt;code&gt;view-transition-name&lt;/code&gt; relevant to the transition currently being executed.&lt;/p&gt;
&lt;p&gt;This avoids a major practical problem of the native property: a rendered element taking part in a named View Transition has one &lt;code&gt;view-transition-name&lt;/code&gt;, and custom names must be unique among participating rendered elements.&lt;/p&gt;
&lt;p&gt;My layer therefore treats names as potential roles rather than permanent CSS state.&lt;/p&gt;
&lt;h3 id=&quot;controlling-participation-and-propagation&quot;&gt;Controlling Participation and Propagation&lt;/h3&gt;
&lt;p&gt;The syntax gradually acquired a few compact controls as real cases appeared.&lt;/p&gt;
&lt;p&gt;The current model can be summarized like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Source syntax  &lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;$panel&lt;/code&gt;       &lt;/td&gt;&lt;td&gt;Start or participate in a transition family&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;$-value&lt;/code&gt;      &lt;/td&gt;&lt;td&gt;Extend the inherited family with &lt;code&gt;value&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;:$panel=&quot;id&quot;&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Add a dynamic instance value&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;$panel!&lt;/code&gt;      &lt;/td&gt;&lt;td&gt;Define the transition here without propagating it further&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;$panel-&lt;/code&gt;      &lt;/td&gt;&lt;td&gt;Establish a family for descendants without making this element participate&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The last two are particularly useful in reusable component trees.&lt;/p&gt;
&lt;p&gt;Sometimes a component is only a structural boundary. Its children need a shared transition namespace, while the component root itself should not get its own snapshot. In other cases a transition applies to one level and should stop before unrelated nested UI starts inheriting it.&lt;/p&gt;
&lt;p&gt;These controls make that decision part of the template rather than additional JavaScript.&lt;/p&gt;
&lt;p&gt;The runtime also supports compact shortcut definitions and the browser's &lt;code&gt;match-element&lt;/code&gt; mode for cases where element identity itself should supply the unique transition matching. Internally, the parser resolves all of these forms into the same transition definition model.&lt;/p&gt;
&lt;h3 id=&quot;the-syntax-exists-only-during-authoring&quot;&gt;The &lt;code&gt;$&lt;/code&gt; Syntax Exists Only During Authoring&lt;/h3&gt;
&lt;p&gt;I did not want unconventional &lt;code&gt;$...&lt;/code&gt; attributes to become the permanent runtime DOM representation.&lt;/p&gt;
&lt;p&gt;A Vue compiler transform rewrites them during compilation.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$modulator-panel
$-amount
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;becomes runtime metadata in the form:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;data-transition-modulator-panel
data-transition--amount
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Additional marker attributes are generated when propagation or parent-only behavior is requested. The transform operates on the Vue compiler AST, including both static attributes and static &lt;code&gt;v-bind&lt;/code&gt; argument names. &lt;/p&gt;
&lt;p&gt;For example, the source-level &lt;code&gt;!&lt;/code&gt; suffix becomes an internal &lt;code&gt;--no-propagate&lt;/code&gt; marker, while a trailing &lt;code&gt;-&lt;/code&gt; on a named family produces a &lt;code&gt;--parent-only&lt;/code&gt; marker. vite-plugin-vue-transition-attr…&lt;/p&gt;
&lt;p&gt;This gives me two representations with different purposes.&lt;/p&gt;
&lt;p&gt;The source representation is optimized for developer experience:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$modulator-panel
$-motion
$-amount
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The DOM representation is explicit and conventional enough for runtime inspection:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;data-transition-modulator-panel
data-transition--motion
data-transition--amount
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;runtime-orchestration&quot;&gt;Runtime Orchestration&lt;/h3&gt;
&lt;p&gt;All imperative orchestration is concentrated in one &lt;code&gt;startTransition()&lt;/code&gt; function.&lt;/p&gt;
&lt;p&gt;Application code can start a transition by name:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await startTransition(`al-setting-${targetId}`, () =&amp;gt; {
    dialogTarget.value = target;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It can also start from a DOM element:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await startTransition(panel, () =&amp;gt; {
    item.amountCurveOn = value;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second form is useful when the caller does not need to know the hierarchy name. The runtime examines the element and its subtree and determines which declared transition names belong to it.&lt;/p&gt;
&lt;p&gt;Multiple prefixes can also be supplied to activate several declarative hierarchies during the same View Transition.&lt;/p&gt;
&lt;p&gt;Internally, &lt;code&gt;startTransition()&lt;/code&gt; performs the lifecycle that I previously had scattered across individual features.&lt;/p&gt;
&lt;p&gt;Before calling &lt;code&gt;document.startViewTransition()&lt;/code&gt;, it resolves the requested prefixes and temporarily assigns &lt;code&gt;viewTransitionName&lt;/code&gt; to matching elements. During the update callback it performs the application state change, waits for Vue's &lt;code&gt;nextTick()&lt;/code&gt;, removes the old assignments, scans the updated DOM, and assigns names again for the new state. After the browser reports the transition as finished, all temporary names are removed.&lt;/p&gt;
&lt;p&gt;The second scan is important. A transition frequently changes which elements exist. The old snapshot and new snapshot therefore cannot rely on a single static set of DOM nodes.&lt;/p&gt;
&lt;p&gt;The declarative attributes remain stable metadata. The runtime derives the correct active names independently on both sides of the DOM update.&lt;/p&gt;
&lt;p&gt;Prefix matching provides another useful property. Starting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;startTransition('modulator-panel-2', update);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;can activate the family rooted at that prefix, including more specific descendants such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;modulator-panel-2-motion
modulator-panel-2-amount
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The runtime performs that matching centrally instead of requiring every call site to enumerate all participating elements.&lt;/p&gt;
&lt;h3 id=&quot;keeping-css-generic&quot;&gt;Keeping CSS Generic&lt;/h3&gt;
&lt;p&gt;The system also assigns a shared &lt;code&gt;view-transition-class&lt;/code&gt; to participating elements.&lt;/p&gt;
&lt;p&gt;This allows generic snapshot behavior to remain very small:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;::view-transition-old(.view-transitioned),
::view-transition-new(.view-transitioned) {
    width: 100%;
    height: 100%;
    object-fit: fill;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;view-transition-class&lt;/code&gt; is designed exactly for this kind of shared styling: unlike &lt;code&gt;view-transition-name&lt;/code&gt;, the class does not need to be unique and can style multiple named transition pseudo-elements. &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/view-transition-class?utm_source=chatgpt.com&quot;&gt;MDN Web Docs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Specific transitions can still have their own CSS where required. The naming, hierarchy, selection, and lifecycle no longer have to be encoded there.&lt;/p&gt;
&lt;h3 id=&quot;editor-tooling-became-part-of-the-design&quot;&gt;Editor Tooling Became Part of the Design&lt;/h3&gt;
&lt;p&gt;Once transition metadata moved into templates, another problem became obvious: attributes disappear visually inside large Vue files.&lt;/p&gt;
&lt;p&gt;I wanted transition structure to be immediately recognizable while reading a component.&lt;/p&gt;
&lt;p&gt;I added a tiny VS Code extension with a TextMate grammar that highlights &lt;code&gt;$...&lt;/code&gt; transition attributes separately from ordinary attributes. The grammar is injected directly into Vue template syntax. transition-attributes.tmLanguage package&lt;/p&gt;
&lt;p&gt;This turned out to be more valuable than I initially expected.&lt;/p&gt;
&lt;p&gt;When opening a large template, I can visually scan the transition structure without tracing JavaScript. Parent families, child segments, and dynamic transition bindings stand out immediately.&lt;/p&gt;
&lt;p&gt;The transition layer became visible as part of the component architecture.&lt;/p&gt;
&lt;h3 id=&quot;generating-transition-names-and-types&quot;&gt;Generating Transition Names and Types&lt;/h3&gt;
&lt;p&gt;The next step was eliminating manually maintained name lists.&lt;/p&gt;
&lt;p&gt;A second Vite plugin scans Vue files, parses each SFC with &lt;code&gt;@vue/compiler-sfc&lt;/code&gt;, parses the template with &lt;code&gt;@vue/compiler-dom&lt;/code&gt;, walks the AST, and collects &lt;code&gt;$...&lt;/code&gt; transition names. It runs initially and again when Vue files are added, changed, or removed. vite-plugin-transition-names&lt;/p&gt;
&lt;p&gt;It then generates a TypeScript source file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export const transitionNames = [
    'al-setting',
    'modulator-panel'
] as const;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That generated constant feeds the runtime type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type TransitionPrefix = typeof transitionNames[number] | `${typeof transitionNames[number]}-${string}`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As a result, creating a new transition family in a template automatically makes the name available to TypeScript autocomplete at &lt;code&gt;startTransition()&lt;/code&gt; call sites. The same generated information also catches many naming mistakes during development.&lt;/p&gt;
&lt;p&gt;There is no separate registry to remember to update.&lt;/p&gt;
&lt;p&gt;The template is the source of truth.&lt;/p&gt;
&lt;h3 id=&quot;the-real-production-case&quot;&gt;The Real Production Case&lt;/h3&gt;
&lt;p&gt;This system grew out of &lt;a rel=&quot;nofollow&quot; href=&quot;https://aleklabs.dev/eye-trainer&quot;&gt;AlekLabs Eye Trainer&lt;/a&gt; rather than an isolated animation experiment.&lt;/p&gt;
&lt;p&gt;A single modulation component already contains several examples: dynamically identified modulator panels, motion and amount children, target settings, slider values, dialog transitions, and transitions started both from DOM elements and from explicit hierarchical prefixes.&lt;/p&gt;
&lt;p&gt;That is the kind of component where a collection of one-off View Transition implementations becomes difficult to maintain. Several independent UI operations coexist, and each operation may involve a different subset of the same component tree.&lt;/p&gt;
&lt;p&gt;With the naming layer, most of that information remains close to the markup.&lt;/p&gt;
&lt;p&gt;The behavioral code describes the state update.&lt;/p&gt;
&lt;p&gt;The template describes transition participation.&lt;/p&gt;
&lt;p&gt;The runtime connects the two.&lt;/p&gt;
&lt;h3 id=&quot;this-is-not-fundamentally-vue-specific&quot;&gt;This Is Not Fundamentally Vue-Specific&lt;/h3&gt;
&lt;p&gt;My current implementation is integrated deeply with Vue because &lt;a rel=&quot;nofollow&quot; href=&quot;https://aleklabs.dev/eye-trainer&quot;&gt;AlekLabs Eye Trainer&lt;/a&gt; uses Vue. The compiler transform uses Vue's compiler packages, the generated-name plugin scans &lt;code&gt;.vue&lt;/code&gt; templates, and the transition runtime waits for &lt;code&gt;nextTick()&lt;/code&gt; before resolving the new DOM.&lt;/p&gt;
&lt;p&gt;The underlying model is framework-independent.&lt;/p&gt;
&lt;p&gt;The browser only sees DOM attributes and &lt;code&gt;document.startViewTransition()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A React, Svelte, Web Components, or plain HTML implementation could use the same architecture with a different authoring transform and a framework-appropriate mechanism for waiting until the DOM update completes.&lt;/p&gt;
&lt;p&gt;The important part is the separation between persistent declarative metadata and temporary native View Transition state.&lt;/p&gt;
&lt;h3 id=&quot;what-i-learned-from-building-it&quot;&gt;What I Learned From Building It&lt;/h3&gt;
&lt;p&gt;The View Transition API itself remained small throughout this process. Most of the engineering work appeared around naming, composition, lifecycle, discoverability, and developer tooling.&lt;/p&gt;
&lt;p&gt;For isolated transitions, direct &lt;code&gt;view-transition-name&lt;/code&gt; declarations are sufficient.&lt;/p&gt;
&lt;p&gt;For a larger application, I found it much more manageable to treat transition participation as its own declarative application layer.&lt;/p&gt;
&lt;p&gt;The final workflow is now straightforward: mark potential participants in the template, compose them into hierarchical namespaces, start a transition from a typed prefix or DOM element, and let one runtime function assign and clean up the browser state.&lt;/p&gt;
&lt;p&gt;Vite handles the source transformation and name discovery. TypeScript provides autocomplete and validation. The editor makes the structure visually recognizable. CSS focuses on the resulting animation.&lt;/p&gt;
&lt;p&gt;This changed View Transitions in &lt;a rel=&quot;nofollow&quot; href=&quot;https://aleklabs.dev/eye-trainer&quot;&gt;AlekLabs Eye Trainer&lt;/a&gt; from feature-specific imperative code into reusable infrastructure.&lt;/p&gt;
&lt;p&gt;And, more importantly for a growing UI, adding the next transition no longer requires inventing another orchestration mechanism.&lt;/p&gt;
&lt;h3 id=&quot;a-useful-side-effect-agent-friendly-transitions&quot;&gt;A Useful Side Effect: Agent-Friendly Transitions&lt;/h3&gt;
&lt;p&gt;A pleasant side effect of this architecture is how well coding agents work with it.&lt;/p&gt;
&lt;p&gt;Once an agent is told that &lt;code&gt;$...&lt;/code&gt; attributes describe View Transition participation, it can usually extend fairly complex transitions without needing to understand the runtime implementation. For many tasks, the instruction can be as small as: use the &lt;code&gt;$&lt;/code&gt; transition attributes to connect these elements into the same transition hierarchy.&lt;/p&gt;
&lt;p&gt;Even transitions involving several components and multiple moving elements can often be implemented without adding feature-specific JavaScript or CSS. The shared runtime already handles name resolution, DOM updates, View Transition lifecycle, and cleanup.&lt;/p&gt;
&lt;p&gt;This also makes agent-generated changes much easier to review. Instead of inspecting a new block of transition-specific JavaScript together with additional CSS selectors and animation rules, I can often review only a few attribute changes in the template. The transition structure is visible directly in the markup, and the diff stays small.&lt;/p&gt;
&lt;p&gt;In practice, modern coding agents handle this declarative syntax very well. Once the pattern exists in the codebase, they can infer the hierarchy from nearby examples and produce complex multi-element transitions with surprisingly little guidance.&lt;/p&gt;
&lt;p&gt;For me, this has become another argument for moving repeated UI behavior into declarative infrastructure: it improves developer experience for humans, and it also gives coding agents a much smaller and more constrained surface to modify.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/6z02zfvid69o3wx4kpba.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30964/a-declarative-naming-layer-for-the-view-transition-api</guid>
<pubDate>Tue, 13 Oct 2026 16:14:47 +0000</pubDate>
</item>
<item>
<title>Container queries: prefer the right container-type for the axis you query</title>
<link>https://coderlegion.com/30906/container-queries-prefer-the-right-container-type-for-the-axis-you-query</link>
<description>&lt;p&gt;Prerequisites&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Basic familiarity with CSS container queries and the container-type and container-name properties.&lt;/li&gt;
&lt;li&gt;Ability to inspect DOM and computed styles in the browser devtools.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Implementation decision&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If your rules include block-axis queries (height, block-size), keep the container that those elements rely on set to container-type: size.&lt;/li&gt;
&lt;li&gt;If you only need inline-axis checks (width), prefer container-type: inline-size to limit containment cost.&lt;/li&gt;
&lt;li&gt;Use container-name to ensure the browser targets the intended ancestor when nested containers could confuse matching.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Expected result&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When the container remains container-type: size, both width- and height-based @container queries will find a valid ancestor and evaluate normally.&lt;/li&gt;
&lt;li&gt;If you switch the same container to container-type: inline-size at runtime, inline-axis (width) queries will still match, but block-axis (height) queries will report no eligible container and stop matching.&lt;/li&gt;
&lt;li&gt;A named container still resolves by name, but a name does not grant missing axis support; axis-based queries still need the right container-type.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Limitation&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The guidance isolates containment eligibility. It does not cover cases where a query fails due to selector specificity, layout overrides, or DOM moves that change ancestors. Always verify the actual DOM path and computed container-type in the browser where the issue appears.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;working-example-1&quot;&gt;Working example&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export function createElement({ tag = 'div', width = 0, height = 0 } = {}) {
  return {
    tag,
    width,
    height,
    parent: null,
    children: [],
    containerType: 'normal',
    containerName: undefined,
  };
}

export function appendChild(parent, child) {
  child.parent = parent;
  parent.children.push(child);
}

export function setContainerType(el, type) {
  el.containerType = type;
}

export function setContainerName(el, name) {
  el.containerName = name;
}

export function toggleContainerType(el, nextType) {
  setContainerType(el, nextType);
}

function compare(op, left, right) {
  switch (op) {
    case '&amp;gt;': return left &amp;gt; right;
    case '&amp;gt;=': return left &amp;gt;= right;
    case '&amp;lt;': return left &amp;lt; right;
    case '&amp;lt;=': return left &amp;lt;= right;
    default: throw new Error(`unsupported compare operator: ${op}`);
  }
}

function queryAxis(query) {
  if (query.kind === 'name-only') return null;
  if (query.axis) return query.axis;
  if (query.feature === 'width') return 'inline';
  if (query.feature === 'height') return 'block';
  return null;
}

export function findEligibleContainer(startEl, query) {
  let node = startEl.parent;
  const axis = queryAxis(query);

  while (node) {
    if (query.kind === 'name-only') {
      if (node.containerName === query.name) return node;
    } else if (axis) {
      if (node.containerType === 'size') return node;
      if (node.containerType === 'inline-size' &amp;amp;&amp;amp; axis === 'inline') return node;
    }
    node = node.parent;
  }

  return null;
}

export function evaluateQuery(targetEl, query) {
  const container = findEligibleContainer(targetEl, query);
  if (!container) {
    return { matched: false, reason: 'no-eligible-container', container: null };
  }

  if (query.kind === 'name-only') {
    return { matched: true, container };
  }

  const axis = queryAxis(query);
  const size = axis === 'inline' ? container.width : container.height;
  return {
    matched: compare(query.compare, size, query.value),
    container,
    containerValue: size,
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read the full article: &lt;a rel=&quot;nofollow&quot; href=&quot;https://chriseugenerodriguez.com/blog/why-do-container-queries-stop-matching-when-you-switch-container-type-dynamically&quot;&gt;https://chriseugenerodriguez.com/blog/why-do-container-queries-stop-matching-when-you-switch-container-type-dynamically&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30906/container-queries-prefer-the-right-container-type-for-the-axis-you-query</guid>
<pubDate>Tue, 13 Oct 2026 08:23:04 +0000</pubDate>
</item>
<item>
<title>Build a Pure CSS Radar Chart — Lightweight, Max Performance</title>
<link>https://coderlegion.com/30746/build-a-pure-css-radar-chart-lightweight-max-performance</link>
<description>&lt;p&gt;Most radar charts pull in SVG path math or a JavaScript charting library. You can skip both.&lt;/p&gt;
&lt;p&gt;With &lt;strong&gt;st-core.fscss&lt;/strong&gt;, a spider (radar) chart is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;an FSCSS array of scores (0–100)&lt;/li&gt;
&lt;li&gt;polar coordinates stored as CSS variables (&lt;code&gt;--st-sxN&lt;/code&gt;, &lt;code&gt;--st-syN&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;shapes drawn with &lt;code&gt;clip-path: polygon()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;rings and spokes from gradients&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No canvas. No SVG. No chart runtime on the critical path.&lt;/p&gt;
&lt;p&gt;Source: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fscss-ttr/st-core.fscss&quot;&gt;github.com/fscss-ttr/st-core.fscss&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;here-s-how-it-works&quot;&gt;Here’s how it works&lt;/h3&gt;
&lt;h4 id=&quot;1-data-is-an-array&quot;&gt;1. Data is an array&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@arr skills[80, 65, 90, 55, 70, 85]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Length can be 5, 6, 8 — whatever you need. Geometry (number of polygon vertices) is fixed when FSCSS expands the mixins.&lt;/p&gt;
&lt;h4 id=&quot;2-one-mixin-writes-coordinates&quot;&gt;2. One mixin writes coordinates&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.spider {
  @st-spider-points(skills, 42)  /* array, max radius % of the box */
  position: relative;
  width: 280px;
  height: 280px;
  border-radius: 50%;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For each index &lt;code&gt;i&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;angle = &lt;code&gt;(i - 1) × 360° / N&lt;/code&gt; (0° at the &lt;strong&gt;top&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;radius = &lt;code&gt;(value / 100) × rmax&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CSS gets:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;--st-sxN: calc(50% + (r * 1%) * sin(θ));
--st-syN: calc(50% - (r * 1%) * cos(θ));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trig stays in the browser’s &lt;code&gt;calc()&lt;/code&gt;. Updating values later is rewriting those variables.&lt;/p&gt;
&lt;h4 id=&quot;3-renderers-only-read-variables&quot;&gt;3. Renderers only read variables&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@st-spider-fill(.spider-fill, skills)
@st-spider-line(.spider-line, skills)
@st-spider-dots(.spider-dot-, skills, 9px)
@st-spider-grid(.spider-grid)
@st-spider-spokes(.spider-spokes, skills)
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Layer &lt;/th&gt;&lt;th&gt;Role&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;fill&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Closed polygon through every &lt;code&gt;--st-sx/sy&lt;/code&gt; pair&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;line&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Dual-pass polygon (outer path + path scaled toward center) so the stroke has real thickness&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;dots&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Absolutely positioned markers at each vertex&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;grid&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Concentric rings (&lt;code&gt;radial-gradient&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;spokes&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Thin radial axes (&lt;code&gt;conic-gradient&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Rule:&lt;/strong&gt; call &lt;code&gt;@st-spider-points&lt;/code&gt; on the &lt;strong&gt;host&lt;/strong&gt;. Children inherit. Same model as linear &lt;code&gt;@st-chart-points&lt;/code&gt; / fill / line in st-core.&lt;/p&gt;
&lt;h4 id=&quot;4-markup-is-plain-html&quot;&gt;4. Markup is plain HTML&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&quot;spider&quot;&amp;gt;
  &amp;lt;div class=&quot;spider-grid&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;spider-spokes&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;spider-fill&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;spider-line&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;spider-dot-1&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;spider-dot-2&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;spider-dot-3&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;spider-dot-4&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;spider-dot-5&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;spider-dot-6&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dots need real nodes — the mixin emits &lt;code&gt;.spider-dot-1&lt;/code&gt; … &lt;code&gt;.spider-dot-N&lt;/code&gt; rules; it does not invent elements.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;here-s-how-values-can-be-displayed-on-the-dot-nodes&quot;&gt;Here’s how values can be displayed on the dot nodes&lt;/h3&gt;
&lt;p&gt;Markers are normal DOM nodes, so you can show the score next to (or inside) each dot without a chart API.&lt;/p&gt;
&lt;h4 id=&quot;option-a-data-value-css-after&quot;&gt;Option A — &lt;code&gt;data-value&lt;/code&gt; + CSS &lt;code&gt;::after&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&quot;spider-dot-1&quot; data-value=&quot;80&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;spider-dot-2&quot; data-value=&quot;65&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;!-- … --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.spider-dot-1,
.spider-dot-2,
.spider-dot-3,
.spider-dot-4,
.spider-dot-5,
.spider-dot-6 {
  /* st-core already sets left/top from --st-sx / --st-sy */
}

[data-value]::after {
  content: attr(data-value);
  position: absolute;
  left: 50%;
  top: -1.4em;
  transform: translateX(-50%);
  font-size: 0.65rem;
  font-variant-numeric: tabular-nums;
  color: var(--st-text);
  white-space: nowrap;
  pointer-events: none;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Push labels slightly outward per axis if they collide with the polygon (different &lt;code&gt;top&lt;/code&gt; / &lt;code&gt;left&lt;/code&gt; offsets per index).&lt;/p&gt;
&lt;h4 id=&quot;option-b-nested-label-element&quot;&gt;Option B — nested label element&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&quot;spider-dot-1&quot;&amp;gt;
  &amp;lt;span class=&quot;dot-label&quot;&amp;gt;80&amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.dot-label {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 4px);
  transform: translateX(-50%);
  font-size: 0.65rem;
  color: var(--st-muted);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;option-c-update-labels-from-js-with-the-series&quot;&gt;Option C — update labels from JS with the series&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function setSeries(host, values, rmax = 42) {
  const n = values.length;
  let css = &quot;&quot;;
  values.forEach((v, i) =&amp;gt; {
    const ang = (i * 360) / n;
    const r = (v / 100) * rmax;
    css += `--st-sx${i + 1}: calc(50% + (${r} * 1%) * sin(${ang} * 1deg));`;
    css += `--st-sy${i + 1}: calc(50% - (${r} * 1%) * cos(${ang} * 1deg));`;
    const dot = host.querySelector(`.spider-dot-${i + 1}`);
    if (dot) {
      dot.dataset.value = v;
      // or: dot.querySelector(&quot;.dot-label&quot;).textContent = v;
    }
  });
  host.style.cssText = css;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Geometry and labels stay in sync because both come from the same array pass.&lt;/p&gt;
&lt;p&gt;Axis names (Speed, Quality, …) stay outside the dots — absolute labels around the circle — so vertices stay readable at small sizes.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;here-s-why-it-s-lighter&quot;&gt;Here’s why it’s lighter&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Traditional radar &lt;/th&gt;&lt;th&gt;st-core radar&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Chart.js / D3 / Recharts bundle &lt;/td&gt;&lt;td&gt;Mixins → CSS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;SVG path string updates &lt;/td&gt;&lt;td&gt;Custom properties + &lt;code&gt;clip-path&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Layout + paint on every redraw &lt;/td&gt;&lt;td&gt;Often compositor-only if you transition &lt;code&gt;clip-path&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Library hydration before first paint &lt;/td&gt;&lt;td&gt;CSS paints as soon as the stylesheet applies&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Compile time:&lt;/strong&gt; FSCSS expands loops into concrete &lt;code&gt;polygon()&lt;/code&gt; stops and selectors. Array &lt;strong&gt;length&lt;/strong&gt; is fixed in the stylesheet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Runtime:&lt;/strong&gt; Changing scores is assigning &lt;code&gt;--st-sxN&lt;/code&gt; / &lt;code&gt;--st-syN&lt;/code&gt; (and optional label text). No path serializer, no virtual DOM for the shape.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Size:&lt;/strong&gt; A compiled spider block is on the order of &lt;strong&gt;~1 kb&lt;/strong&gt; of CSS for the chart rules — not a 50–200 kb chart dependency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance profile:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First paint: no JS required for a static series  &lt;/li&gt;
&lt;li&gt;Updates: style recalc + composite; avoid layout thrash if size is fixed  &lt;/li&gt;
&lt;li&gt;Multi-series: extra layers are extra divs + inherited variables, not extra chart instances&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s “max performance” in the practical sense: &lt;strong&gt;minimum work per frame and minimum bytes on the wire.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;here-are-some-use-cases-of-spider-radar-charts&quot;&gt;Here are some use cases of spider (radar) charts&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Skills / competency matrices&lt;/strong&gt; — Speed, Quality, UX, Scale, Cost, Support for a team or candidate.  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Product scorecards&lt;/strong&gt; — Compare features across dimensions (performance, price, ecosystem, docs, support).  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sprint or OKR health&lt;/strong&gt; — Delivery, quality, collaboration, predictability on one glanceable shape.  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security / compliance posture&lt;/strong&gt; — Control families as axes; one polygon per system or vendor.  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Player or character stats&lt;/strong&gt; — Games and education UIs where polar layout is the expected metaphor.  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Brand or perception studies&lt;/strong&gt; — Trust, value, innovation, service — survey averages as radii.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Anywhere humans already think in “balanced vs spiky” profiles, a radar chart communicates faster than six separate bars.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;here-s-how-to-do-multi-series-radar-charts-with-still-pure-css&quot;&gt;Here’s how to do multi-series radar charts with still pure CSS&lt;/h3&gt;
&lt;p&gt;Each series is a &lt;strong&gt;separate host&lt;/strong&gt; with its own &lt;code&gt;@st-spider-points&lt;/code&gt;. Shared grid and spokes sit underneath.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@import((*) from st-core@v2)
@st-root()
@st-spider-root()

@arr seriesA[70, 55, 40, 85, 90]
@arr seriesB[45, 75, 60, 50, 55]
@arr seriesC[85, 40, 70, 35, 65]

@st-spider-fill(.fill-a, seriesA)
@st-spider-line(.line-a, seriesA)
@st-spider-dots(.dot-a-, seriesA, 8px)

@st-spider-fill(.fill-b, seriesB)
@st-spider-line(.line-b, seriesB)
@st-spider-dots(.dot-b-, seriesB, 8px)

@st-spider-fill(.fill-c, seriesC)
@st-spider-line(.line-c, seriesC)
@st-spider-dots(.dot-c-, seriesC, 8px)

@st-spider-grid(.spider-grid)
@st-spider-spokes-n(.multi-spokes, 5)

.multi-spider {
  position: relative;
  width: 320px;
  height: 320px;
  border-radius: 50%;
  background: var(--st-surface);
}

.series-a {
  @st-spider-points(seriesA, 42)
  position: absolute;
  inset: 0;
  --st-accent: #ff9f43;
  --st-spider-fill-opacity: 20%;
}
.series-b {
  @st-spider-points(seriesB, 42)
  position: absolute;
  inset: 0;
  --st-accent: #54a0ff;
  --st-spider-fill-opacity: 16%;
}
.series-c {
  @st-spider-points(seriesC, 42)
  position: absolute;
  inset: 0;
  --st-accent: #ff6b9d;
  --st-spider-fill-opacity: 18%;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&quot;multi-spider&quot;&amp;gt;
  &amp;lt;div class=&quot;spider-grid&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;multi-spokes&quot;&amp;gt;&amp;lt;/div&amp;gt;

  &amp;lt;div class=&quot;series-a&quot;&amp;gt;
    &amp;lt;div class=&quot;fill-a&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;line-a&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;dot-a-1&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;dot-a-2&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;dot-a-3&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;dot-a-4&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;dot-a-5&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;

  &amp;lt;div class=&quot;series-b&quot;&amp;gt;
    &amp;lt;div class=&quot;fill-b&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;line-b&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;dot-b-1&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;!-- … --&amp;gt;
  &amp;lt;/div&amp;gt;

  &amp;lt;div class=&quot;series-c&quot;&amp;gt;
    &amp;lt;div class=&quot;fill-c&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;line-c&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;dot-c-1&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;!-- … --&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why this stays pure CSS:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No JS chart instance per series  &lt;/li&gt;
&lt;li&gt;Isolation is DOM + custom properties, not a scene graph  &lt;/li&gt;
&lt;li&gt;Opacity and accent are tokens (&lt;code&gt;--st-spider-fill-opacity&lt;/code&gt;, &lt;code&gt;--st-accent&lt;/code&gt;)  &lt;/li&gt;
&lt;li&gt;Spoke count can match the axis count via &lt;code&gt;@st-spider-spokes-n(..., 5)&lt;/code&gt; without tying spokes to one series’ array&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If two series shared one host and only one &lt;code&gt;@st-spider-points&lt;/code&gt; call, the last write would win and every series would draw the same shape. Separate hosts fix that — the same multi-series rule as linear st-core charts.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;quick-start&quot;&gt;Quick start&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;FSCSS &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Figsh/xfscss&quot;&gt;CLI&lt;/a&gt;, Vscode &lt;a rel=&quot;nofollow&quot; href=&quot;https://marketplace.visualstudio.com/items?itemName=Figsh.fscss&quot;&gt;extention&lt;/a&gt;, or prototyping via its &lt;a rel=&quot;nofollow&quot; href=&quot;https://fscss.devtem.org/&quot;&gt;cdn&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@import((*) from st-core@v2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Full API in the repo: &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fscss-ttr/st-core.fscss&quot;&gt;github.com/fscss-ttr/st-core.fscss&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Pure CSS radar charts are not a compromise. With st-core they’re a &lt;strong&gt;styling problem&lt;/strong&gt;: arrays in, variables out, polygons on screen — lightweight by construction, fast by default.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/30746/build-a-pure-css-radar-chart-lightweight-max-performance</guid>
<pubDate>Sun, 11 Oct 2026 20:07:46 +0000</pubDate>
</item>
<item>
<title>Stop setting your dark mode background to #000000 and text to #FFFFFF</title>
<link>https://coderlegion.com/30736/stop-setting-your-dark-mode-background-to-000000-and-text-to-ffffff</link>
<description>&lt;p&gt;Pure black on pure white creates high chromatic contrast that causes &lt;em&gt;eye strain&lt;/em&gt; and &lt;em&gt;severe halation&lt;/em&gt; for readers with astigmatism.&lt;/p&gt;
&lt;p&gt;A proper dark theme uses layered &lt;strong&gt;dark grays/slates (#121212, #1E1E1E)&lt;/strong&gt; to express depth through elevation shadows and tinted surfaces rather than stark, blinding contrast.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What are your favorite color palettes for comfortable dark mode reading?&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/30736/stop-setting-your-dark-mode-background-to-000000-and-text-to-ffffff</guid>
<pubDate>Sun, 11 Oct 2026 18:40:37 +0000</pubDate>
</item>
<item>
<title>Debugging scroll anchoring when a prepended feed shifts</title>
<link>https://coderlegion.com/30510/debugging-scroll-anchoring-when-a-prepended-feed-shifts</link>
<description>&lt;p&gt;If a feed prepends new rows while someone is reading older ones, inspect the scroll container before adding JavaScript to adjust its offset. Chromium may already be compensating for the inserted height through scroll anchoring. A transform on the relevant path can suppress that compensation, producing a jump even though the prepend code did not change.&lt;/p&gt;
&lt;h3 id=&quot;reproduce-the-two-states&quot;&gt;Reproduce the two states&lt;/h3&gt;
&lt;p&gt;Save this as &lt;code&gt;scroll-anchor.html&lt;/code&gt; and open it in Chromium. Click &lt;strong&gt;Prepend&lt;/strong&gt; after the list settles. Then open the same file with &lt;code&gt;?suppressed=1&lt;/code&gt; and repeat. The two runs differ only in the transform applied immediately before the insertion.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;button id=&quot;prepend&quot;&amp;gt;Prepend&amp;lt;/button&amp;gt;
&amp;lt;div id=&quot;viewport&quot; class=&quot;viewport&quot;&amp;gt;&amp;lt;ol id=&quot;feed&quot;&amp;gt;&amp;lt;/ol&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;style&amp;gt;
  .viewport { height: 300px; overflow: auto; }
  .viewport.transform-shift { transform: translateY(0); }
  #feed li { min-height: 72px; }
&amp;lt;/style&amp;gt;
&amp;lt;script&amp;gt;
  const viewport = document.querySelector('#viewport');
  const feed = document.querySelector('#feed');
  for (let index = 0; index &amp;lt; 30; index++) {
    const row = document.createElement('li');
    row.textContent = `Message ${index + 1}`;
    feed.append(row);
  }
  feed.children[16].scrollIntoView({ block: 'center' });

  document.querySelector('#prepend').addEventListener('click', () =&amp;gt; {
    const before = viewport.scrollTop;
    if (location.search.includes('suppressed=1')) {
      viewport.classList.add('transform-shift');
    }
    const row = document.createElement('li');
    row.textContent = 'New message';
    feed.prepend(row);
    requestAnimationFrame(() =&amp;gt; {
      console.log({ before, after: viewport.scrollTop });
    });
  });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With anchoring enabled, the browser should increase &lt;code&gt;scrollTop&lt;/code&gt; to keep the same message near its prior viewport position. With the transform applied, the controlled Chromium fixture used for the original article leaves the offset unchanged, so visible content moves. Compare both the measured offset and the message in view; a single number alone can hide what the reader experienced.&lt;/p&gt;
&lt;h3 id=&quot;decide-whether-to-keep-anchoring&quot;&gt;Decide whether to keep anchoring&lt;/h3&gt;
&lt;p&gt;If the feed should preserve the current reading position, investigate transforms on the scroller and its ancestors before adding manual scroll correction. If movement is intentional, &lt;code&gt;overflow-anchor: none&lt;/code&gt; is the explicit CSS opt-out. Apply it only to the section where that behavior is wanted, then retest insertion and focus behavior. Avoid disabling anchoring across a whole page to hide a local layout problem.&lt;/p&gt;
&lt;p&gt;This example is a small browser fixture, not a recipe for virtualized lists. The recorded independent test ran in Chromium on Linux; it does not establish identical results in other browsers or in a framework's scrolling abstraction. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Scroll_anchoring/Overview&quot;&gt;MDN scroll anchoring overview&lt;/a&gt; describes suppression triggers, and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/overflow-anchor&quot;&gt;MDN &lt;code&gt;overflow-anchor&lt;/code&gt; reference&lt;/a&gt; covers the opt-out.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://chriseugenerodriguez.com/blog/why-does-prepending-items-change-scroll-position-unless-you-disable-scroll-anchoring&quot;&gt;Read the full article and view the tested screenshots&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30510/debugging-scroll-anchoring-when-a-prepended-feed-shifts</guid>
<pubDate>Fri, 09 Oct 2026 19:59:45 +0000</pubDate>
</item>
<item>
<title>Halving Codebase Duplication in Static Site Generators</title>
<link>https://coderlegion.com/30169/halving-codebase-duplication-in-static-site-generators</link>
<description>&lt;p&gt;In large-scale static site architectures, especially projects serving thousands of programmatic SEO &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/engineering-high-intent-activity-landing-pages/&quot;&gt;landing pages&lt;/a&gt; such as 2,993 generated routes across duration views and multilingual hubs, markup duplication tends to grow invisibly. When a codebase scales across multiple sibling pages and shared layout files, repetitive boilerplate markup and stylesheet rules accumulate without immediate detection. During a recent static analysis audit using SonarCloud and jscpd, our codebase exhibited an overall duplication rate of 10.36 percent, representing 5,749 duplicated lines across 182 distinct clone clusters. Legal and compliance views each contained identical multi-line style blocks styling document layouts. Timer surface modals and interactive drawers were duplicated verbatim across four key presentation and timer routes. Stage widget markup, including announcement carousels and embedded video players, was repeated across various views. Global theme stylesheets duplicated numerous identical lines of CSS variables between default light theme selectors. While quality gates on new code passed initially, the cumulative technical debt posed high maintenance overhead and pushed repository metrics uncomfortably close to the 50,000 line free-tier ceiling of our static analysis tools. This article explores how to systematically detect, isolate, and collapse that duplication while keeping test suites fully green.&lt;/p&gt;
&lt;h3 id=&quot;architectural-decision-and-trade-offs&quot;&gt;Architectural Decision and Trade-offs&lt;/h3&gt;
&lt;p&gt;Instead of attempting to micro-optimize small utility functions, we applied a high-impact Pareto extraction strategy. This approach focused on targeting large block clones first, since roughly 80 percent of codebase duplication typically resides in a small fraction of files, specifically inline modal dialogs, drawers, and repeated stylesheet blocks. We extracted all 12 modal backdrops and the realtime cheer drawer into a clean, dedicated component named TimerModals.astro consisting of 821 lines. We then extracted shared stage widgets like announcement slides and video players into StageWidgets.astro, parameterized with flexible props for internationalized layouts. Centralizing legal styling into a dedicated legal.css file imported once per document further reduced inline redundancy. In native CSS, we merged default light theme variables and light data-theme selectors into a unified selector block, leveraging CSS specificity to eliminate duplicate declarations without breaking dark mode operating system toggles. To protect test invariants during these refactorings, assertions that previously scanned raw template strings for specific DOM identifiers were upgraded to dynamically resolve child component imports, maintaining comprehensive coverage without coupling tests to strict file structures.&lt;/p&gt;
&lt;h3 id=&quot;implementing-component-extraction-and-css-merging&quot;&gt;Implementing Component Extraction and CSS Merging&lt;/h3&gt;
&lt;p&gt;To see how this works in practice, consider the structural organization of duplicated modal templates in static site generators. Instead of repeating identical modal markup across multiple layout files, isolate the markup into a single parameterized component and import it where needed. The following example demonstrates how an Astro template imports a consolidated modal component and a centralized stylesheet:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
import TimerModals from '../components/timer/TimerModals.astro';
import StageWidgets from '../components/timer/StageWidgets.astro';
import '../styles/legal.css';

const { pageTitle } = Astro.props;
---
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;{pageTitle}&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;main&amp;gt;
      &amp;lt;StageWidgets slidesLabel=&quot;Announcement&quot; videoLabel=&quot;Player&quot; /&amp;gt;
    &amp;lt;/main&amp;gt;
    &amp;lt;TimerModals /&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, CSS token redundancy can be solved by merging duplicate variable declarations. Instead of maintaining separate blocks for root and explicit light themes, combine them into a single rule set while handling dark mode through media queries:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:root, [data-theme=&quot;light&quot;] {
  --color-bg: #ffffff;
  --color-text: #111111;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme=&quot;light&quot;]) {
    --color-bg: #0b0f19;
    --color-text: #f3f4f6;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This method keeps client DOM element identifiers stable, ensuring that existing client-side scripts, accessibility attributes, and end-to-end test selectors continue to function without requiring rewrites of interaction logic.&lt;/p&gt;
&lt;h3 id=&quot;verification-and-test-suite-integrity&quot;&gt;Verification and Test Suite Integrity&lt;/h3&gt;
&lt;p&gt;Refactoring massive template blocks carries the inherent risk of breaking downstream layouts or test assertions. To validate our deduplication effort without regressions, we executed a rigorous verification checklist across multiple validation tools. Running the duplication detector confirmed that repository duplication dropped from 10.36 percent down to 5.43 percent across the entire project. Total line count settled at 49,321 lines, placing the repository safely below the strict 50,000 line free-tier boundary. Unit test suites containing 109 individual assertions passed with zero failures. Linter and type check passes reported zero errors across all component files. Finally, executing 96 Playwright end-to-end browser interaction flows confirmed that all modal dialogs, drawer toggles, and UI components behaved correctly in a rendered browser environment. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/executing-a-thorough-technical-project-review/&quot;&gt;Executing A Thorough Technical Project Review&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Systematic codebase deduplication in static site generators requires targeting large structural blocks rather than chasing minor utility redundancies. By isolating repetitive modal dialogs into reusable components, unifying theme tokens, and updating test harnesses to verify composed trees, engineering teams can significantly lower duplication rates. This discipline preserves strict code analysis quotas, reduces maintenance overhead, and ensures that large programmatic websites remain robust and performant as they scale.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30169/halving-codebase-duplication-in-static-site-generators</guid>
<pubDate>Thu, 08 Oct 2026 00:09:06 +0000</pubDate>
</item>
<item>
<title>Animating a 3D Gift Box with WebGPU</title>
<link>https://coderlegion.com/29724/animating-a-3d-gift-box-with-webgpu</link>
<description>&lt;p&gt;&lt;strong&gt;What is WebGPU, in one paragraph?&lt;/strong&gt;&lt;br&gt;
WebGPU is a modern browser API that gives JavaScript direct, low-overhead access to your graphics card.&lt;/p&gt;
&lt;p&gt;It replaces WebGL, which was built mainly for drawing.&lt;br&gt;
WebGPU is modeled on native APIs like Vulkan, Metal and Direct3D 12, and it adds compute shaders, so the GPU can run any parallel math, not just triangles. That’s why it matters for AI: neural networks are mostly giant matrix multiplications, which is exactly what GPUs are good at.&lt;/p&gt;
&lt;p&gt;Learn more :-&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://tutorialsavvy.com/animating-a-3d-gift-box-with-webgpu-80acd1d38842&quot;&gt;WebGPU is a modern browser API&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29724/animating-a-3d-gift-box-with-webgpu</guid>
<pubDate>Sun, 04 Oct 2026 12:49:20 +0000</pubDate>
</item>
<item>
<title>FOUC-Proof Retro Design Systems</title>
<link>https://coderlegion.com/29604/fouc-proof-retro-design-systems</link>
<description>&lt;p&gt;When building web interfaces with a retro arcade aesthetic, developers often adopt vibrant neon color palettes to recreate an 80s terminal feel. However, applying these bright accents directly across both light and dark surfaces introduces two critical problems. First, neon shades like electric cyan frequently fail minimum WCAG contrast requirements against pale backgrounds, dropping below the 4.5 to 1 ratio needed for accessibility. Second, relying on client-side JavaScript or late-loading framework components to manage theme state causes a jarring Flash of Unstyled Content or white flash during document parsing. Solving these issues requires combining asymmetric color calibration with synchronous pre-render execution in static site generators like Astro. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/android-offline-first-event-pipeline/&quot;&gt;Offline First Event Pipeline&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-pitfall-of-retro-aesthetics-on-the-web&quot;&gt;The Pitfall of Retro Aesthetics on the Web&lt;/h3&gt;
&lt;p&gt;Nostalgic design systems rely heavily on high-contrast neon tones to establish character. While electric cyan or neon green looks striking against a dark matte chassis, using those exact same hex codes on a light background destroys readability. A color that passes contrast checks in a dark environment will often wash out completely on a light surface. Attempting to fix this by handcoding individual theme styles across disparate components leads to code duplication and visual fragmentation. Maintaining a cohesive brand identity requires a structured token architecture that adapts semantically depending on the active theme mode.&lt;/p&gt;
&lt;h3 id=&quot;the-mathematics-of-accessible-contrast&quot;&gt;The Mathematics of Accessible Contrast&lt;/h3&gt;
&lt;p&gt;To keep a consistent visual identity without sacrificing readability, the color system must use dual-role semantic token calibration. Instead of forcing identical accent values in both modes, the primary accent variable shifts based on the document theme attribute. In light mode, the system switches to a calibrated deep arcade teal that achieves a WCAG AAA contrast ratio against pale surfaces. In dark mode, it returns to the high-vibrancy electric cyan. Secondary accents, such as a high-energy orange tone, serve consistently as action indicators. The following CSS custom property configuration demonstrates how to structure these semantic tokens at the root level: For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/audit-macos-system-data-before-deleting-developer-caches/&quot;&gt;Audit Macos System Data Before Deleting&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:root {
  --surface: #fbfcfc;
  --surface-raised: #f0f2f5;
  --text-main: #111418;
  --accent: #007e85;
  --accent-secondary: #ff5722;
  --line: #d1d5db;
}

[data-theme=&quot;dark&quot;] {
  --surface: #0e1114;
  --surface-raised: #161b22;
  --text-main: #f0f6fc;
  --accent: #00adb5;
  --accent-secondary: #ff5722;
  --line: #30363d;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;eliminating-fouc-with-synchronous-head-scripts&quot;&gt;Eliminating FOUC with Synchronous Head Scripts&lt;/h3&gt;
&lt;p&gt;Even with well-structured CSS variables, waiting for external stylesheets or framework hydration to apply the correct theme attribute creates visual flicker. Client-side scripts that execute inside &lt;code&gt;DOMContentLoaded&lt;/code&gt; or standard component lifecycles run too late in the rendering pipeline. To eliminate this delay, a blocking script must execute synchronously inside the document head before the body layout paints. By checking &lt;code&gt;localStorage&lt;/code&gt; or &lt;code&gt;prefers-color-scheme&lt;/code&gt; immediately, the browser can set the target attribute and background color instantly. The following component snippet illustrates how to implement this pattern in an Astro layout:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script is:inline&amp;gt;
  const storedTheme = localStorage.getItem('theme');
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  const theme = storedTheme || (prefersDark ? 'dark' : 'light');
  
  document.documentElement.setAttribute('data-theme', theme);
  if (theme === 'dark') {
    document.documentElement.style.backgroundColor = '#0e1114';
  } else {
    document.documentElement.style.backgroundColor = '#fbfcfc';
  }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;maintaining-a-single-source-of-truth&quot;&gt;Maintaining a Single Source of Truth&lt;/h3&gt;
&lt;p&gt;Scaling a token-based design system across static sites and standalone tools requires clear organizational boundaries. Core platform utilities like extension showcases or isolated calculators may demand independent UI scopes, but editorial and reading surfaces must consume global design variables uniformly. Desktop reading aids, active bullet indicators, and border lines should bind directly to semantic variables like &lt;code&gt;--accent&lt;/code&gt; and &lt;code&gt;--line&lt;/code&gt; rather than hardcoded color values. Keeping these token mappings documented in a versioned reference file ensures consistency across design vaults and code repositories without introducing runtime overhead.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29604/fouc-proof-retro-design-systems</guid>
<pubDate>Sat, 03 Oct 2026 09:06:20 +0000</pubDate>
</item>
<item>
<title>FSCSS 1.2.5 ships property shorthands: size, px, inset-x, ratio-fit()</title>
<link>https://coderlegion.com/29321/fscss-1-2-5-ships-property-shorthands-size-px-inset-x-ratio-fit</link>
<description>&lt;p&gt;A new FSCSS release. This one's aimed at the pairs everyone keeps typing out by hand:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* before */
width: 32px;
height: 32px;

/* after */
size: 32px;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same deal for the other repeat offenders:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.card {
  px: 16px;      /* padding-left + padding-right */
  py: 14px;      /* padding-top + padding-bottom */
  mx: auto;      /* margin-left + margin-right */
  max-w: 360px;
}

.overlay-side {
  pos: absolute;
  inset-x: 45px; /* left + right only, top/bottom untouched */
  top: 0;
  h: 4px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;inset-x&lt;/code&gt; one is the one I actually wanted to exist. &lt;code&gt;inset: auto 45px&lt;/code&gt; sets all four sides, there's no native way to say &quot;just left and right&quot; without writing &lt;code&gt;left&lt;/code&gt; and &lt;code&gt;right&lt;/code&gt; separately. Now there is.&lt;/p&gt;
&lt;p&gt;There's also &lt;code&gt;ratio-fit()&lt;/code&gt; for the fill-container-keep-aspect-ratio pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.media &amp;gt; img {
  inset: ratio-fit(16 / 9);
  obj-fit: cover;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compiles ahead of time, no runtime polyfill needed for any of it if you're already compiling with the CLI.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install fscss@1.2.5
fscss input.fscss output.css
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also ships &lt;code&gt;stack&lt;/code&gt;, &lt;code&gt;hstack&lt;/code&gt;, &lt;code&gt;center&lt;/code&gt;, &lt;code&gt;auto-fit&lt;/code&gt;, &lt;code&gt;truncate&lt;/code&gt;, &lt;code&gt;line-clamp&lt;/code&gt;, border/background shorthands, and the usual aliases (&lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;fw&lt;/code&gt;, &lt;code&gt;jc&lt;/code&gt;, &lt;code&gt;ai&lt;/code&gt;, &lt;code&gt;pos&lt;/code&gt;, &lt;code&gt;shadow&lt;/code&gt;...). Full map's linked below. There's a real UI using a chunk of these together, a dual-phone budget app template combining this with modules like &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fscss-ttr/st-core.fscss&quot;&gt;st-core@v2 charts&lt;/a&gt;, if you want to see it in context rather than isolated snippets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Figsh/xfscss/releases/tag/v1.2.5&quot;&gt;https://github.com/Figsh/xfscss/releases/tag/v1.2.5&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Shorthands reference:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://fscss.devtem.org/shorthands&quot;&gt;https://fscss.devtem.org/shorthands&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Budget app template:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fscss-ttr/st-core.fscss/tree/main/templates/budget-app&quot;&gt;https://github.com/fscss-ttr/st-core.fscss/tree/main/templates/budget-app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Anyone using &lt;code&gt;size&lt;/code&gt;/&lt;code&gt;px&lt;/code&gt;/&lt;code&gt;inset-x&lt;/code&gt; already hit any edge cases worth flagging?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29321/fscss-1-2-5-ships-property-shorthands-size-px-inset-x-ratio-fit</guid>
<pubDate>Wed, 30 Sep 2026 23:48:53 +0000</pubDate>
</item>
<item>
<title>🚨 CSS Specificity — The Hidden Reason Your UI Breaks</title>
<link>https://coderlegion.com/29269/css-specificity-the-hidden-reason-your-ui-breaks</link>
<description>&lt;p&gt;Most developers learn CSS specificity once.&lt;/p&gt;
&lt;p&gt;They remember:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;#id &amp;gt; .class &amp;gt; div
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then move on.&lt;/p&gt;
&lt;p&gt;Until one day…&lt;/p&gt;
&lt;p&gt;Everything looks correct.&lt;/p&gt;
&lt;p&gt;The CSS is present.&lt;/p&gt;
&lt;p&gt;The selector is correct.&lt;/p&gt;
&lt;p&gt;The z-index looks higher.&lt;/p&gt;
&lt;p&gt;And yet the UI is broken.&lt;/p&gt;
&lt;p&gt;That’s when CSS specificity stops being a beginner topic and becomes a production debugging problem.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-production-incident-that-started-this&quot;&gt;The Production Incident That Started This&lt;/h2&gt;
&lt;p&gt;Recently, I was working on a microfrontend application.&lt;/p&gt;
&lt;p&gt;Everything worked fine initially.&lt;/p&gt;
&lt;p&gt;I opened a page, launched a modal and the UI looked correct.&lt;/p&gt;
&lt;p&gt;Then I navigated to another microfrontend.&lt;/p&gt;
&lt;p&gt;Its CSS got loaded.&lt;/p&gt;
&lt;p&gt;After returning to the original microfrontend, suddenly:&lt;/p&gt;
&lt;p&gt;❌ Modal appeared behind page content&lt;br&gt;
❌ Overlay behaved incorrectly&lt;br&gt;
❌ z-index looked correct but wasn't working&lt;/p&gt;
&lt;p&gt;DevTools showed my CSS rule still existed.&lt;/p&gt;
&lt;p&gt;Yet another CSS rule was winning.&lt;/p&gt;
&lt;p&gt;The culprit?&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;CSS Specificity.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-what-is-css-specificity-&quot;&gt;🧠 What is CSS Specificity?&lt;/h2&gt;
&lt;p&gt;CSS specificity is the algorithm browsers use to decide:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Which CSS rule wins when multiple rules target the same element.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Browsers don't simply apply:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;The last CSS rule.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's one of the biggest misconceptions.&lt;/p&gt;
&lt;p&gt;Specificity is calculated first.&lt;/p&gt;
&lt;p&gt;Only when specificity is equal does source order become important.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-example&quot;&gt;⚔️ Example&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.modal {
  z-index: 9999;
}

.some-library .modal {
  z-index: 100;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&quot;some-library&quot;&amp;gt;
  &amp;lt;div class=&quot;modal&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Many developers expect:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.modal
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to win because the value is larger.&lt;/p&gt;
&lt;p&gt;But CSS doesn't compare values first.&lt;/p&gt;
&lt;p&gt;It compares selectors.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-how-specificity-works&quot;&gt;🧮 How Specificity Works&lt;/h2&gt;
&lt;p&gt;Specificity is usually represented as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;ID - CLASS - TYPE
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;specificity-table&quot;&gt;Specificity Table&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Selector        &lt;/th&gt;&lt;th&gt;Specificity&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;*&lt;/code&gt;             &lt;/td&gt;&lt;td&gt;0-0-0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;div&lt;/code&gt;           &lt;/td&gt;&lt;td&gt;0-0-1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.modal&lt;/code&gt;        &lt;/td&gt;&lt;td&gt;0-1-0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;[type=&quot;text&quot;]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;0-1-0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;:hover&lt;/code&gt;        &lt;/td&gt;&lt;td&gt;0-1-0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;#dialog&lt;/code&gt;       &lt;/td&gt;&lt;td&gt;1-0-0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Inline Style    &lt;/td&gt;&lt;td&gt;Highest&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;MDN defines specificity as the weight browsers calculate to determine which declaration gets applied when multiple selectors match the same element.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;example-calculation&quot;&gt;Example Calculation&lt;/h2&gt;
&lt;p&gt;Selector:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;button.primary
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Contains:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;button     → 0-0-1
.primary   → 0-1-0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Total:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;0-1-1
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;Another selector:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;#header button.primary
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Contains:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;#header   → 1-0-0
button    → 0-0-1
.primary  → 0-1-0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Total:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;1-1-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This selector wins.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-myth-1-last-css-always-wins&quot;&gt;❌ Myth #1 — Last CSS Always Wins&lt;/h2&gt;
&lt;p&gt;Many developers believe:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The CSS rule written last wins.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Not always.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;#header {
  color: red;
}

div {
  color: blue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div id=&quot;header&quot;&amp;gt;
  Hello
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Red
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why?&lt;/p&gt;
&lt;p&gt;Because:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;#header = 1-0-0
div     = 0-0-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Specificity wins before source order.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-myth-2-higher-z-index-always-wins&quot;&gt;❌ Myth #2 — Higher z-index Always Wins&lt;/h2&gt;
&lt;p&gt;This causes countless production bugs.&lt;/p&gt;
&lt;p&gt;Developers often write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.modal {
  z-index: 99999;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And expect it to appear above everything.&lt;/p&gt;
&lt;p&gt;Not necessarily.&lt;/p&gt;
&lt;p&gt;Because:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;z-index works inside stacking contexts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-what-is-a-stacking-context-&quot;&gt;🧠 What is a Stacking Context?&lt;/h2&gt;
&lt;p&gt;A stacking context is a separate layering environment.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.parent {
  position: relative;
  z-index: 1;
}

.child {
  position: absolute;
  z-index: 999999;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;999999
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the child cannot escape its parent's stacking context.&lt;/p&gt;
&lt;p&gt;This is why modal, tooltip, dropdown, and popover bugs often feel confusing.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-microfrontend-problem&quot;&gt;The Microfrontend Problem&lt;/h2&gt;
&lt;p&gt;This is where specificity becomes extremely important.&lt;/p&gt;
&lt;p&gt;Imagine:&lt;/p&gt;
&lt;h4 id=&quot;microfrontend-a&quot;&gt;Microfrontend A&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.modal {
  z-index: 9999;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;microfrontend-b&quot;&gt;Microfrontend B&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.layout .modal {
  z-index: 100;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Specificity:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.modal
0-1-0

.layout .modal
0-2-0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Microfrontend B wins.&lt;/p&gt;
&lt;p&gt;Even if your CSS is still loaded.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-my-fix-worked&quot;&gt;Why My Fix Worked&lt;/h2&gt;
&lt;p&gt;Originally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.modal {
  z-index: 9999;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But another selector had higher specificity.&lt;/p&gt;
&lt;p&gt;So I wrapped the modal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;&amp;lt;div class=&quot;dialog-wrapper&quot;&amp;gt;
  &amp;lt;Modal /&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.dialog-wrapper .modal {
  z-index: 9999;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.modal
0-1-0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.dialog-wrapper .modal
0-2-0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now my selector became more specific and started winning.&lt;/p&gt;
&lt;p&gt;The CSS value never changed.&lt;/p&gt;
&lt;p&gt;Only specificity changed.&lt;/p&gt;
&lt;p&gt;And the issue disappeared.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-why-this-happens-more-in-microfrontends&quot;&gt;🚨 Why This Happens More in Microfrontends&lt;/h2&gt;
&lt;p&gt;Microfrontends often share:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Same DOM&lt;/li&gt;
&lt;li&gt;Same global CSS&lt;/li&gt;
&lt;li&gt;Same class names&lt;/li&gt;
&lt;li&gt;Same browser environment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Problems include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CSS leakage&lt;/li&gt;
&lt;li&gt;Style collisions&lt;/li&gt;
&lt;li&gt;Unexpected overrides&lt;/li&gt;
&lt;li&gt;Load-order changes after navigation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes specificity bugs significantly more common.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;modern-css-specificity-features-developers-often-miss&quot;&gt;Modern CSS Specificity Features Developers Often Miss&lt;/h2&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1-is-&quot;&gt;1️⃣ &lt;code&gt;:is()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:is(.btn, .link)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;:is()&lt;/code&gt; pseudo-class itself doesn't add specificity.&lt;/p&gt;
&lt;p&gt;Instead, it takes the specificity of the most specific selector inside it.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:is(.btn, #header)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Specificity:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;1-0-0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;#header
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;is most specific.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2-not-&quot;&gt;2️⃣ &lt;code&gt;:not()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Many developers think:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:not(.hidden)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;adds nothing.&lt;/p&gt;
&lt;p&gt;Not true.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;:not()&lt;/code&gt; itself doesn't contribute specificity.&lt;/p&gt;
&lt;p&gt;But selectors inside it do.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:not(.hidden)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Specificity:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;0-1-0
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3-where-&quot;&gt;3️⃣ &lt;code&gt;:where()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;One of the most underrated CSS features.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:where(.btn)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Specificity:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;0-0-0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Always.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;:where()&lt;/code&gt; intentionally has zero specificity.&lt;/p&gt;
&lt;p&gt;This makes it extremely useful for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Design systems&lt;/li&gt;
&lt;li&gt;Component libraries&lt;/li&gt;
&lt;li&gt;Shared UI frameworks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because consumers can easily override styles.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;how-to-debug-css-specificity-issues&quot;&gt;How to Debug CSS Specificity Issues&lt;/h2&gt;
&lt;p&gt;When CSS isn't applying:&lt;/p&gt;
&lt;p&gt;Check these in order:&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-is-the-rule-present-&quot;&gt;1. Is the rule present?&lt;/h3&gt;
&lt;p&gt;Open DevTools.&lt;/p&gt;
&lt;p&gt;Verify the CSS exists.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-is-it-crossed-out-&quot;&gt;2. Is it crossed out?&lt;/h3&gt;
&lt;p&gt;Crossed-out CSS usually means:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Another rule won.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-which-selector-is-winning-&quot;&gt;3. Which selector is winning?&lt;/h3&gt;
&lt;p&gt;DevTools shows the winning selector.&lt;/p&gt;
&lt;p&gt;Inspect it carefully.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-compare-specificity&quot;&gt;4. Compare specificity&lt;/h3&gt;
&lt;p&gt;Many bugs become obvious here.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-check-css-load-order&quot;&gt;5. Check CSS load order&lt;/h3&gt;
&lt;p&gt;When specificity is equal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Last rule wins.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-check-stacking-contexts&quot;&gt;6. Check stacking contexts&lt;/h3&gt;
&lt;p&gt;Especially for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Modals&lt;/li&gt;
&lt;li&gt;Dropdowns&lt;/li&gt;
&lt;li&gt;Tooltips&lt;/li&gt;
&lt;li&gt;Popovers&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-check-portals&quot;&gt;7. Check portals&lt;/h3&gt;
&lt;p&gt;React portals often render outside expected DOM hierarchy.&lt;/p&gt;
&lt;p&gt;This changes how CSS behaves.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-best-practices&quot;&gt;🎯 Best Practices&lt;/h2&gt;
&lt;p&gt;✅ Prefer class selectors over IDs&lt;/p&gt;
&lt;p&gt;✅ Avoid deeply nested selectors&lt;/p&gt;
&lt;p&gt;✅ Use low-specificity CSS in shared libraries&lt;/p&gt;
&lt;p&gt;✅ Use &lt;code&gt;:where()&lt;/code&gt; for easily overridable styles&lt;/p&gt;
&lt;p&gt;✅ Be careful with global CSS in microfrontends&lt;/p&gt;
&lt;p&gt;✅ Avoid excessive &lt;code&gt;!important&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Most developers think CSS specificity is a beginner topic.&lt;/p&gt;
&lt;p&gt;In reality:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;It's one of the most common reasons production UIs break unexpectedly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Especially in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Large applications&lt;/li&gt;
&lt;li&gt;Design systems&lt;/li&gt;
&lt;li&gt;Microfrontends&lt;/li&gt;
&lt;li&gt;Shared component libraries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The tricky part is that the CSS often looks correct.&lt;/p&gt;
&lt;p&gt;The rule exists.&lt;/p&gt;
&lt;p&gt;The value is correct.&lt;/p&gt;
&lt;p&gt;And yet another selector silently wins.&lt;/p&gt;
&lt;p&gt;Understanding specificity deeply transforms CSS debugging from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Trial and Error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;into:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Predictable Engineering
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;key-takeaways&quot;&gt;Key Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CSS does not simply apply the last rule&lt;/li&gt;
&lt;li&gt;Specificity determines which selector wins&lt;/li&gt;
&lt;li&gt;Higher z-index does not always mean higher visibility&lt;/li&gt;
&lt;li&gt;Microfrontends make specificity issues more common&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:is()&lt;/code&gt;, &lt;code&gt;:not()&lt;/code&gt;, and &lt;code&gt;:where()&lt;/code&gt; have special specificity behavior&lt;/li&gt;
&lt;li&gt;&lt;code&gt;!important&lt;/code&gt; should be a last resort&lt;/li&gt;
&lt;li&gt;DevTools can quickly reveal specificity conflicts when used correctly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Understanding CSS specificity is one of those skills that feels minor — until it saves you hours of debugging a production UI issue.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29269/css-specificity-the-hidden-reason-your-ui-breaks</guid>
<pubDate>Wed, 30 Sep 2026 16:07:21 +0000</pubDate>
</item>
<item>
<title>FSCSS auto-loop: percentage keyframes</title>
<link>https://coderlegion.com/28689/fscss-auto-loop-percentage-keyframes</link>
<description>&lt;p&gt;This snippet builds a &lt;strong&gt;stepped “processing 0% → 99% → done”&lt;/strong&gt; animation by &lt;strong&gt;looping an array into &lt;code&gt;@keyframes&lt;/code&gt; percentages&lt;/strong&gt; — not a JS timer.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;npm install -g fscss@latest&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;VS Code: &lt;strong&gt;FSCSS&lt;/strong&gt; language support (&lt;a rel=&quot;nofollow&quot; href=&quot;https://marketplace.visualstudio.com/items?itemName=Figsh.fscss&quot;&gt;FSCSS Support&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;File: &lt;code&gt;my-first-fscss.fscss&lt;/code&gt; → compile or runtime-load&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;source-your-idea-tightened-&quot;&gt;Source (your idea, tightened)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;h2::after {
  content: &quot;...&quot;;
  animation: count 10s steps(100) forwards;
}

/* 1, 2, … 99  (auto list for keyframe stops) */
@arr numlist[count(99)]

@define counter(start:ing..., process:ing..., end:ed){`
  0% {
    content: &quot;@use(start)0%&quot;;
  }
  @arr.numlist[]% {
    content: &quot;@use(process)@arr.numlist[]%&quot;;
  }
  100% {
    content: &quot;@use(end)&quot;;
  }
`}

@keyframes count {
  @counter(Processing..., Processing..., Processed)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-auto-loop-is-here&quot;&gt;What “auto-loop” is here&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Piece &lt;/th&gt;&lt;th&gt;Role&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;count(99)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Builds a numeric list used as &lt;strong&gt;1–99&lt;/strong&gt; (engine: &lt;code&gt;count(n)&lt;/code&gt; / &lt;code&gt;count(n, 1)&lt;/code&gt; → index list).&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;@arr numlist[…]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Stores that list.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;@arr.numlist[]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Auto-index&lt;/strong&gt;: expand once per item.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;@arr.numlist[]%&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Each item becomes a &lt;strong&gt;keyframe selector&lt;/strong&gt;: &lt;code&gt;1%&lt;/code&gt;, &lt;code&gt;2%&lt;/code&gt;, … &lt;code&gt;99%&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;@define counter(…)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Template for the keyframe body; &lt;code&gt;@use(start|process|end)&lt;/code&gt; fills labels.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;@counter(…)&lt;/code&gt; inside &lt;code&gt;@keyframes&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Injects the whole ladder into &lt;code&gt;count&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So the loop is &lt;strong&gt;compile-time expansion&lt;/strong&gt;, not &lt;code&gt;for&lt;/code&gt; in the browser.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;breakdown&quot;&gt;Breakdown&lt;/h3&gt;
&lt;h4 id=&quot;1-target&quot;&gt;1. Target&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;h2::after {
  content: &quot;...&quot;;
  animation: count 10s steps(100) forwards;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Animates &lt;strong&gt;&lt;code&gt;content&lt;/code&gt;&lt;/strong&gt; on the pseudo-element.  &lt;/li&gt;
&lt;li&gt;&lt;code&gt;steps(100)&lt;/code&gt; ≈ one visual step per percent over 10s.  &lt;/li&gt;
&lt;li&gt;&lt;code&gt;forwards&lt;/code&gt; keeps the final &lt;code&gt;Processed&lt;/code&gt; text.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-index-array&quot;&gt;2. Index array&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@arr numlist[count(99)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same family as modules indexes: a list long enough to drive stops. Here each value is also the &lt;strong&gt;percentage number&lt;/strong&gt; in the output.&lt;/p&gt;
&lt;h4 id=&quot;3-reusable-keyframe-block&quot;&gt;3. Reusable keyframe block&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@define counter(start:…, process:…, end:…){` … `}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;0%&lt;/code&gt;&lt;/strong&gt; — intro label + &lt;code&gt;0%&lt;/code&gt;.  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@arr.numlist[]%&lt;/code&gt;&lt;/strong&gt; — for each &lt;code&gt;n&lt;/code&gt; in &lt;code&gt;numlist&lt;/code&gt;, emit:&lt;ul&gt;
&lt;li&gt;selector: &lt;code&gt;n%&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;content: &quot;&amp;lt;process&amp;gt;n%&quot;&lt;/code&gt;  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;100%&lt;/code&gt;&lt;/strong&gt; — final label only (e.g. &lt;code&gt;Processed&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;4-wire-into-a-named-animation&quot;&gt;4. Wire into a named animation&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@keyframes count {
  @counter(Processing..., Processing..., Processed)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expands to a long &lt;code&gt;@keyframes count { … }&lt;/code&gt; with ~100 content stops.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;shape-of-compiled-css-illustrative-&quot;&gt;Shape of compiled CSS (illustrative)&lt;/h3&gt;
&lt;p&gt;Exact quoting depends on the compiler; structure is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;h2::after {
  content: &quot;...&quot;;
  animation: count 10s steps(100) forwards;
}

@keyframes count {
  0% {
    content: &quot;Processing...0%&quot;;
  }
  1% {
    content: &quot;Processing...1%&quot;;
  }
  2% {
    content: &quot;Processing...2%&quot;;
  }
  /* … */
  99% {
    content: &quot;Processing...99%&quot;;
  }
  100% {
    content: &quot;Processed&quot;;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the auto-loop: &lt;strong&gt;one array walk → many keyframe rules&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-html&quot;&gt;Example HTML&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
  &amp;lt;title&amp;gt;FSCSS auto-loop counter&amp;lt;/title&amp;gt;
  &amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/fscss@1.2.4/runtime.min.js&quot; async&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;link type=&quot;text/fscss&quot; href=&quot;my-first-fscss.fscss&quot;&amp;gt;
  &amp;lt;!-- or: &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;my-first-fscss.css&quot;&amp;gt; after CLI --&amp;gt;
  &amp;lt;style&amp;gt;
    body {
      font-family: system-ui, sans-serif;
      padding: 2rem;
      background: #0f172a;
      color: #e2e8f0;
    }
    h2 {
      font-size: 1.25rem;
      font-weight: 600;
    }
    h2::after {
      margin-left: 0.35em;
      color: #38bdf8;
    }
  &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;h2&amp;gt;Status&amp;lt;/h2&amp;gt;
  &amp;lt;!-- ::after shows Processing...0% → … → Processed --&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;compile&quot;&gt;Compile&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fscss my-first-fscss.fscss my-first-fscss.css
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;mental-model&quot;&gt;Mental model&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;count(99)  →  list of stops
@arr.x[]%  →  “for each item, emit item% { … }”
@define    →  reuse labels (start / process / end)
@keyframes →  holds the expanded ladder
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Auto-loop&lt;/strong&gt; = array auto-index used as &lt;strong&gt;keyframe percentages&lt;/strong&gt; (or selectors elsewhere), so one definition drives N rules.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;variants&quot;&gt;Variants&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Progress label only: change &lt;code&gt;@use(process)&lt;/code&gt; strings.  &lt;/li&gt;
&lt;li&gt;Fewer steps: &lt;code&gt;count(20)&lt;/code&gt; + shorter animation / &lt;code&gt;steps(20)&lt;/code&gt;.  &lt;/li&gt;
&lt;li&gt;Same pattern for staggered selectors: &lt;code&gt;*Emails are not allowed*[] { animation-delay: … }&lt;/code&gt; (as in arrays docs).&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28689/fscss-auto-loop-percentage-keyframes</guid>
<pubDate>Sat, 26 Sep 2026 07:18:17 +0000</pubDate>
</item>
<item>
<title>Web design trends 2026: what is worth adopting</title>
<link>https://coderlegion.com/28601/web-design-trends-2026-what-is-worth-adopting</link>
<description>&lt;p&gt;Most of the 2026 trend coverage describes a real shift: a reaction against the flawless, interchangeable look that AI-generated design produced at scale. Read the roundups from Figma, Webflow and Wix side by side and the same instinct runs through all of them. Make it look like a person decided this.&lt;/p&gt;
&lt;p&gt;That reaction contains some things worth adopting, some worth adopting carefully, and a few that will look dated by next summer. Trend lists are written by designers for designers, so they rarely say what a trend costs once it has to be built, made responsive, kept accessible and maintained by people who never saw the mockup. That is the part I want to cover here, trend by trend, from the side of the person who builds it.&lt;/p&gt;
&lt;h3 id=&quot;bento-grids-adopt&quot;&gt;Bento grids: adopt&lt;/h3&gt;
&lt;p&gt;Modular tiles of different sizes, popularised by Apple's product pages and now standard on SaaS marketing sites. It is the rare trend that is structurally useful. Content of unequal importance gets unequal space, the tiles reflow cleanly on small screens, and adding a feature later does not mean redesigning the section.&lt;/p&gt;
&lt;p&gt;It is also cheap to build, which is not true of most things on these lists. CSS Grid does nearly all of the work: a few tiles spanning two columns or two rows, a single column below a breakpoint, and nothing that needs JavaScript. If the grid lives inside a component that appears in more than one place, container queries let each tile respond to the space it actually has rather than to the viewport, and every current browser supports them.&lt;/p&gt;
&lt;p&gt;One trap is worth knowing before you reach for &lt;code&gt;grid-auto-flow: dense&lt;/code&gt;. It fills the holes by pulling later tiles up into earlier gaps, so the visual order can stop matching the source order, and keyboard focus follows the source. For tiles that are just content that is fine. For tiles that are links or buttons, a user tabbing through the grid will jump around it.&lt;/p&gt;
&lt;p&gt;The design failure is uniformity. Twelve identical tiles is a table pretending to be a bento grid. The layout works when the size differences mean something, when the biggest tile really is the most important thing, and it needs real content to look designed rather than diagrammatic. Build it with placeholder text and it will look fine in review and empty in production.&lt;/p&gt;
&lt;h3 id=&quot;typography-as-the-interface-adopt-carefully&quot;&gt;Typography as the interface: adopt carefully&lt;/h3&gt;
&lt;p&gt;Very large, viewport-scaled type carrying the page instead of imagery. Figma lists bold typography among its trends, and Webflow's version is dynamic text treatments. There is an engineering argument alongside the aesthetic one: text weighs almost nothing next to a hero image, so a type-led page can be both striking and fast. When the headline is the largest thing on the screen, it becomes the page's Largest Contentful Paint element, and a block of text paints long before a large photograph finishes downloading.&lt;/p&gt;
&lt;p&gt;The care is in the details, and they are exactly the kind a design file does not show. Type sized in &lt;code&gt;vw&lt;/code&gt; alone does not grow when someone zooms, because zooming shrinks the viewport by the same factor. Mixing a &lt;code&gt;rem&lt;/code&gt; term into the preferred value of a &lt;code&gt;clamp()&lt;/code&gt; helps, but it does not guarantee much: Adrian Roselli's tests show fluid type growing noticeably less than plain type at 200% zoom, and text that cannot reach twice its original size is a potential failure of WCAG's resize-text criterion. The only way to know is to zoom your own page to 200% and look.&lt;/p&gt;
&lt;p&gt;The rest is ordinary typography that the trend tempts people to forget. Give the headline a minimum that stays readable on a phone and a maximum that stops it eating a wide monitor. Keep body copy to a comfortable line length, somewhere around &lt;code&gt;65ch&lt;/code&gt;, and do not let body text shrink below about 16 pixels to make a huge headline look even bigger. Done without those constraints, it is an accessibility problem wearing a trend's clothes.&lt;/p&gt;
&lt;h3 id=&quot;tactile-brutalism-and-intentional-imperfection-depends-who-you-are&quot;&gt;Tactile brutalism and intentional imperfection: depends who you are&lt;/h3&gt;
&lt;p&gt;Raw geometry, hard 1px borders, stark type, visible texture, hand-drawn marks, unpolished photography. Wix files it under elevated brutalism and tactile maximalism, Figma under neo-brutalism. The motivation is explicit: proving a human made this, at a moment when flawless is free. For agencies, designers and brands selling craft or personality, it works and it differentiates.&lt;/p&gt;
&lt;p&gt;For a law firm, a medical practice, a manufacturer or a financial service, deliberate roughness reads as carelessness rather than authorship. Trust-sensitive sectors pay for legibility and calm, not for evidence of hands. Know which one you are before buying the aesthetic.&lt;/p&gt;
&lt;p&gt;The build cost is low, with one thing to watch: how the texture arrives. Grain done as a full-screen overlay with a blend mode has to be blended again on every frame you scroll, and that bill is paid on the slowest phone that visits. A small tiled image gets the same look for almost nothing. The rest of the style is a gift to accessibility rather than a threat to it, because hard borders and stark black on white bring their contrast with them.&lt;/p&gt;
&lt;h3 id=&quot;anti-grid-and-organic-layouts-usually-skip&quot;&gt;Anti-grid and organic layouts: usually skip&lt;/h3&gt;
&lt;p&gt;Flowing shapes, overlapping elements, deliberately broken alignment. They look superb in a portfolio shot and they are expensive to build responsively, because every breakpoint is a bespoke decision rather than a rule. Overlap usually means absolute positioning or negative margins, and both break the moment the content changes. A longer heading, a translated label or one extra card, and the composition collapses.&lt;/p&gt;
&lt;p&gt;If the site is a showcase and the content is fixed, it can be worth it. On a site that has to survive a marketing team adding sections for three years, a disciplined grid with a few deliberate breaks gets most of the effect at a fraction of the maintenance. Grid makes those breaks cheap, too. Two items can share the same grid lines and overlap without either leaving the layout, which is the version of broken alignment that survives a content change.&lt;/p&gt;
&lt;h3 id=&quot;micro-interactions-adopt-but-functionally&quot;&gt;Micro-interactions: adopt, but functionally&lt;/h3&gt;
&lt;p&gt;The consensus has moved from motion as decoration to motion as feedback: a button that acknowledges a press, a field that responds to input, a state change you can follow. Figma lists motion design among its trends. That is good, and it is cheap.&lt;/p&gt;
&lt;p&gt;Two constraints keep it good. The first is &lt;code&gt;prefers-reduced-motion&lt;/code&gt;, which every current browser supports. For people with vestibular disorders, heavy animation is a real barrier rather than a preference. Reduced does not have to mean none, either: swapping a slide for a short fade usually keeps the feedback and drops the movement.&lt;/p&gt;
&lt;p&gt;The second is the interaction cost. Interaction to Next Paint, the Core Web Vital that succeeded First Input Delay, measures how long the page takes to paint after a tap, a click or a key press, and Google counts 200 milliseconds or less as good. Animation that runs JavaScript on the main thread during input is exactly what that metric punishes, so an effect meant to make the page feel responsive can measurably make it less so. Animating &lt;code&gt;transform&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt; in CSS keeps most of that work off the main thread. Animating layout properties from a click or scroll handler puts it straight back.&lt;/p&gt;
&lt;h3 id=&quot;what-i-would-skip-outright&quot;&gt;What I would skip outright&lt;/h3&gt;
&lt;p&gt;Scroll-jacking, where the page takes over scrolling to run a sequence. It breaks user expectation, keyboard navigation and screen readers, and it has been dying for years without quite finishing. Webflow's 2026 list has a trend called guided scrolling, and it is worth reading closely, because it describes the opposite: progress indicators and visual cues that show where you are while the scroll stays yours. That version is fine. The one that decides how fast you are allowed to move down the page is not.&lt;/p&gt;
&lt;p&gt;Heavy 3D on a landing page, unless the product genuinely is the visual. Figma's list opens with 3D and immersive elements, and for a shoe you can spin or a room you can walk through, it earns its weight. Elsewhere the cost is real and rarely repaid. A WebGL &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; is not even eligible to be the Largest Contentful Paint element, so the scene itself is never what the metric waits for. What it does is compete with the element that is, for bandwidth and for the main thread. On our own site the three.js scenes are loaded with a dynamic import and kept out of the initial bundle entirely, so the headline and the text never wait for them.&lt;/p&gt;
&lt;p&gt;Cursor effects, which do not exist on a phone or a tablet, where a large share of your traffic arrives.&lt;/p&gt;
&lt;p&gt;And AI-generated stock imagery. Twelve months ago it read as modern. It now reads as cheap, precisely because everyone has seen the same aesthetic. Real photographs of real things, even imperfect ones, are the safer bet for trust, which is the whole point of the intentional-imperfection trend.&lt;/p&gt;
&lt;h3 id=&quot;the-part-that-is-not-a-trend&quot;&gt;The part that is not a trend&lt;/h3&gt;
&lt;p&gt;Every year the list changes, and the two things that decide whether a site performs do not: it loads fast, and everyone can use it. A trend-perfect site that takes four seconds to show its headline, or fails keyboard navigation, loses to a plain one that does neither. Spend the design budget on the trends that survive that test — bento structure, type-led pages, functional motion — and treat the rest as taste.&lt;/p&gt;
&lt;p&gt;That is also the question worth asking of any trend before it goes into a build. What does it cost at the breakpoints nobody mocked up, in the language nobody designed for, on the slowest phone that will visit? The good ones are cheap in all three places. The rest are cheap only in the screenshot.&lt;/p&gt;
&lt;h3 id=&quot;the-original&quot;&gt;The original&lt;/h3&gt;
&lt;p&gt;This is the longer, build-side version of a guide on our site. The original is the short version for the people paying for the design, and it ends with the questions clients ask about all this: whether to follow trends at all, how often a business site should be redesigned, whether trendy designs hurt SEO, and whether minimalism is finished.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://levelui.com/resources/web-design-trends-2026?utm_source=coderlegion&amp;amp;utm_medium=referral&amp;amp;utm_campaign=web-design-trends-2026&quot;&gt;Web design trends 2026: what is worth adopting&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28601/web-design-trends-2026-what-is-worth-adopting</guid>
<pubDate>Fri, 25 Sep 2026 12:24:02 +0000</pubDate>
</item>
<item>
<title>Layout Grids in CSS: Designing with Millimeter Precision Before Writing Code</title>
<link>https://coderlegion.com/26517/layout-grids-in-css-designing-with-millimeter-precision-before-writing-code</link>
<description>&lt;p&gt;If you find yourself constantly guessing &lt;code&gt;gap&lt;/code&gt;, &lt;code&gt;padding&lt;/code&gt;, and &lt;code&gt;margin&lt;/code&gt; values in your browser inspect tool, you are likely skipping a fundamental step: &lt;strong&gt;establishing a layout grid system before writing layout CSS&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In traditional editorial design, grids provide visual harmony, alignment, and proportional scaling across print pages. In frontend engineering, a mathematical layout grid bridges the gap between raw CSS containers and intentional visual design.&lt;/p&gt;
&lt;h5 id=&quot;1-the-8pt-spatial-system&quot;&gt;&lt;strong&gt;1. The 8pt Spatial System&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;The most versatile framework for modern web interfaces is the &lt;strong&gt;8pt grid rule&lt;/strong&gt; (8px, 16px, 24px, 32px, 48px, 64px). Because most screen dimensions are divisible by 8, aligning component padding, margins, and line heights to multiples of 8 creates natural visual rhythm.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* Predictable Spatial System Tokens */
:root {
  --space-1: 0.25rem; /* 4px */
  --space-2: 0.5rem;  /* 8px */
  --space-3: 1rem;    /* 16px */
  --space-4: 1.5rem;  /* 24px */
  --space-5: 2rem;    /* 32px */
  --space-6: 3rem;    /* 48px */
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;2-moving-beyond-hardcoded-layout-spans&quot;&gt;&lt;strong&gt;2. Moving Beyond Hardcoded Layout Spans&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;Instead of setting rigid pixel widths on individual components, leverage CSS Grid with explicit fractional track ratios (&lt;code&gt;fr&lt;/code&gt;) and fluid min-max clamping. This mirrors professional editorial layout grids directly in the DOM.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* Clean Editorial Grid Layout */
.grid-editorial {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--space-4);
  max-width: 1200px;
  margin-inline: auto;
  padding-inline: var(--space-3);
}

/* Main Content Spans 8 Columns, Sidebar Spans 4 Columns */
.main-content {
  grid-column: span 12;
}

.sidebar {
  grid-column: span 12;
}

@media (min-width: 768px) {
  .main-content {
    grid-column: span 8;
  }
  
  .sidebar {
    grid-column: span 4;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;3-fluid-alignment-with-clamp-&quot;&gt;&lt;strong&gt;3. Fluid Alignment with `clamp()&lt;/strong&gt;`&lt;/h5&gt;
&lt;p&gt;To maintain precision across screens without writing dozens of media queries, combine grid gap logic with mathematical fluid scaling using &lt;code&gt;clamp()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.card-grid {
  display: grid;
  /* Automatically fits items with a minimum width of 280px */
  grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  /* Fluid spacing that scales continuously between mobile and desktop */
  gap: clamp(1rem, 2.5vw, 2rem);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;systemic-spacing-beats-trial-and-error&quot;&gt;&lt;strong&gt;Systemic Spacing Beats Trial and Error&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;When you build web layouts using an explicit grid math system, you stop tweaking CSS margins by trial and error. Your layouts become predictable, accessible, and resilient across screen sizes.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;call-to-action-cta-&quot;&gt;&lt;strong&gt;Call to Action (CTA)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Do you use strict spatial systems (like the 8pt grid) in your stylesheets, or do you adjust layout spacing visually as you build?&lt;/strong&gt; Let’s discuss in the comments!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(Published by Joseph Salaki — Visual Designer &amp;amp; Frontend Engineer at &lt;a rel=&quot;nofollow&quot; href=&quot;https://byjoemetry.com&quot;&gt;byjoemetry.com&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26517/layout-grids-in-css-designing-with-millimeter-precision-before-writing-code</guid>
<pubDate>Tue, 22 Sep 2026 06:00:00 +0000</pubDate>
</item>
<item>
<title>How to Use st-core.fscss with Svelte</title>
<link>https://coderlegion.com/27953/how-to-use-st-core-fscss-with-svelte</link>
<description>&lt;p&gt;st-core.fscss draws charts with pure CSS, &lt;code&gt;clip-path&lt;/code&gt; polygons plus custom properties, no JS chart library, no SVG, no canvas. Svelte doesn't need to draw anything for it. It just needs to own the data and reactively set &lt;code&gt;--st-p1&lt;/code&gt;, &lt;code&gt;--st-p2&lt;/code&gt;, and so on whenever your series changes. Wanted to write up the actual integration because getting the responsibilities split correctly took me a minute the first time through.&lt;/p&gt;
&lt;h4 id=&quot;why-compiled-mode-not-the-runtime-cdn&quot;&gt;Why compiled mode, not the runtime CDN&lt;/h4&gt;
&lt;p&gt;Svelte already has a build pipeline. Shipping the FSCSS runtime to a Svelte app just to expand a few chart mixins into static CSS is unnecessary work for the browser to do on every load. The runtime CDN script is fine for a CodePen or a quick static HTML demo, but for an actual Svelte or SvelteKit app you compile once at build time and import plain CSS.&lt;/p&gt;
&lt;p&gt;Three layers, each with one job:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Layer &lt;/th&gt;&lt;th&gt;Job&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;FSCSS CLI &lt;/td&gt;&lt;td&gt;Expand &lt;code&gt;@st-chart-fill&lt;/code&gt; / &lt;code&gt;@st-chart-line&lt;/code&gt; into static CSS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Svelte &lt;/td&gt;&lt;td&gt;Reactive &lt;code&gt;data[]&lt;/code&gt; → &lt;code&gt;--st-pN&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Browser CSS &lt;/td&gt;&lt;td&gt;Paint, plus optional &lt;code&gt;clip-path&lt;/code&gt; transitions&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Svelte state  →  style=&quot;--st-p1: 30%; --st-p2: 45%; …&quot;
                      ↓
              compiled st-core.css
                      ↓
                 chart on screen
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;1-write-the-fscss-entry-file&quot;&gt;1. Write the FSCSS entry file&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;src/styles/st-core.fscss&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@import((*) from st-core@v2)

@st-root()

/* Length at compile time = number of polygon stops */
@arr chartData[0, 0, 0, 0, 0, 0, 0]

@st-chart-fill(.chart-fill, chartData)
@st-chart-line(.chart-line, chartData)

.chart {
  @st-chart-points(chartData)
  position: relative;
  width: 100%;
  height: 220px;
  background: var(--st-surface);
  border-radius: var(--st-radius-lg);
  overflow: hidden;
}

.chart-fill,
.chart-line {
  transition: clip-path 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The zeros are placeholders, they only matter for their count. Svelte overwrites the real Y values through CSS variables at runtime. What you can't change without recompiling is the array's &lt;strong&gt;length&lt;/strong&gt; — match it to your series (7 days of data → 7 entries).&lt;/p&gt;
&lt;h4 id=&quot;2-compile-it-as-part-of-your-build&quot;&gt;2. Compile it as part of your build&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -D fscss@latest
npx fscss src/styles/st-core.fscss src/styles/st-core.css
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wire it into &lt;code&gt;package.json&lt;/code&gt; so it runs automatically before every build:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;scripts&quot;: {
    &quot;build:st-core&quot;: &quot;fscss src/styles/st-core.fscss src/styles/st-core.css&quot;,
    &quot;prebuild&quot;: &quot;npm run build:st-core&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then import the compiled CSS once, wherever makes sense for your app (root layout, &lt;code&gt;app.css&lt;/code&gt;, or the chart's parent component):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import '../styles/st-core.css';
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-map-your-data-to---st-pn&quot;&gt;3. Map your data to &lt;code&gt;--st-pN&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Chart geometry in st-core is top-down, the same way CSS itself is. A high value needs to sit near &lt;code&gt;0%&lt;/code&gt;, not &lt;code&gt;100%&lt;/code&gt;. So every point gets inverted:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;--st-pᵢ = (100 - humanValue) %
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A score of &lt;code&gt;90&lt;/code&gt; ends up &lt;code&gt;10%&lt;/code&gt; from the top, which is exactly where you'd expect it visually.&lt;/p&gt;
&lt;p&gt;Here's a reusable &lt;code&gt;Chart.svelte&lt;/code&gt; that does that mapping reactively, Svelte 4 style:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;lt;script&amp;gt;
  /** @type {number[]} 0–100, higher = taller */
  export let data = [];
  export let height = '200px';
  export let color = '#9d7eff';

  $: cssVars = [
    ...data.map((value, index) =&amp;gt; `--st-p${index + 1}: ${100 - value}%`),
    `--st-accent: ${color}`,
    `height: ${height}`
  ].join('; ');
&amp;lt;/script&amp;gt;

&amp;lt;div class=&quot;chart&quot; style={cssVars}&amp;gt;
  &amp;lt;div class=&quot;chart-fill&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;chart-line&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the Svelte 5 version, using &lt;code&gt;$props&lt;/code&gt; / &lt;code&gt;$derived&lt;/code&gt; instead of the old export-let / reactive-statement pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;lt;script&amp;gt;
  let { data = [], height = '200px', color = '#9d7eff' } = $props();

  let cssVars = $derived(
    [
      ...data.map((value, index) =&amp;gt; `--st-p${index + 1}: ${100 - value}%`),
      `--st-accent: ${color}`,
      `height: ${height}`
    ].join('; ')
  );
&amp;lt;/script&amp;gt;

&amp;lt;div class=&quot;chart&quot; style={cssVars}&amp;gt;
  &amp;lt;div class=&quot;chart-fill&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;chart-line&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both do the exact same job, just with whichever reactivity API your Svelte version uses.&lt;/p&gt;
&lt;h4 id=&quot;4-use-it-like-any-other-component&quot;&gt;4. Use it like any other component&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;lt;script&amp;gt;
  import './styles/st-core.css';
  import Chart from './lib/Chart.svelte';

  let weekly = [45, 70, 30, 90, 60, 80, 50];

  function randomize() {
    weekly = Array.from({ length: 7 }, () =&amp;gt; Math.floor(Math.random() * 80) + 10);
  }
&amp;lt;/script&amp;gt;

&amp;lt;Chart data={weekly} color=&quot;#4fffb0&quot; height=&quot;250px&quot; &amp;gt;

&amp;lt;!-- Svelte 4 --&amp;gt;
&amp;lt;button type=&quot;button&quot; on:click={randomize}&amp;gt;Randomize&amp;lt;/button&amp;gt;

&amp;lt;!-- Svelte 5 --&amp;gt;
&amp;lt;!-- &amp;lt;button type=&quot;button&quot; onclick={randomize}&amp;gt;Randomize&amp;lt;/button&amp;gt; --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reassigning &lt;code&gt;weekly&lt;/code&gt; recomputes &lt;code&gt;cssVars&lt;/code&gt;, the browser applies the new custom properties, and the compiled CSS's &lt;code&gt;clip-path&lt;/code&gt; transition animates the shape change. No chart library re-rendering, no SVG path recalculation, no manual DOM diffing on your end.&lt;/p&gt;
&lt;h4 id=&quot;fixed-length-vs-changing-length&quot;&gt;Fixed length vs changing length&lt;/h4&gt;
&lt;p&gt;Because geometry is baked in at compile time, the array length is the one thing you can't change on the fly:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Situation &lt;/th&gt;&lt;th&gt;What to do&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Always exactly &lt;strong&gt;N&lt;/strong&gt; points &lt;/td&gt;&lt;td&gt;Compile &lt;code&gt;@arr chartData&lt;/code&gt; with &lt;strong&gt;N&lt;/strong&gt; zeros, always pass exactly &lt;strong&gt;N&lt;/strong&gt; values from Svelte&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Length varies, up to a max of &lt;strong&gt;M&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Compile &lt;strong&gt;M&lt;/strong&gt; zeros, then &lt;code&gt;data.slice(0, M)&lt;/code&gt; and pad shorter series with &lt;code&gt;0&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you compile a template expecting 5 points and hand it 8 values from Svelte, the extra &lt;code&gt;--st-p6&lt;/code&gt;, &lt;code&gt;--st-p7&lt;/code&gt;, &lt;code&gt;--st-p8&lt;/code&gt; have nowhere to go, there's no polygon stop for them until you recompile with a longer array.&lt;/p&gt;
&lt;h4 id=&quot;what-not-to-do-in-production&quot;&gt;What not to do in production&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- Fine for a demo, avoid as your only path in a real Svelte app --&amp;gt;
&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/fscss@1.2.3/runtime.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Runtime mode is for CodePen and quick static HTML, not for a build pipeline that already exists. See the plain &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fscss-ttr/st-core.fscss/tree/main/integration/html&quot;&gt;HTML integration sample&lt;/a&gt; if that's what you actually want.&lt;/p&gt;
&lt;h4 id=&quot;the-mental-model-condensed&quot;&gt;The mental model, condensed&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;@st-chart-points&lt;/code&gt;, fill, and line define the &lt;strong&gt;structure&lt;/strong&gt; and only ever read &lt;code&gt;--st-p*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Svelte's only job is to &lt;strong&gt;replace&lt;/strong&gt; those variables when the data changes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;transition: clip-path&lt;/code&gt; gives you motion for free, no animation library needed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Nothing here is Svelte-specific past step 3, the same pattern maps directly to Vue, React, or vanilla JS, only the reactivity layer changes. Contributions for other frameworks welcome under &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fscss-ttr/st-core.fscss/tree/main/integration&quot;&gt;&lt;code&gt;integration/&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fscss-ttr/st-core.fscss&quot;&gt;github.com/fscss-ttr/st-core.fscss&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Svelte sample:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fscss-ttr/st-core.fscss/tree/main/integration/svelte&quot;&gt;integration/svelte&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27953/how-to-use-st-core-fscss-with-svelte</guid>
<pubDate>Mon, 21 Sep 2026 12:45:40 +0000</pubDate>
</item>
<item>
<title>Finally Shipped st-core.fscss v2 — CSS Charts Without the 8-Point Ceiling</title>
<link>https://coderlegion.com/27679/finally-shipped-st-core-fscss-v2-css-charts-without-the-8-point-ceiling</link>
<description>&lt;p&gt;For a while, st-core.fscss charts had one annoying limit: every line, fill, and dot was locked to exactly 8 data points. &lt;code&gt;--st-p1&lt;/code&gt; through &lt;code&gt;--st-p8&lt;/code&gt;, hardcoded. Want to plot 12 weeks of revenue? Tough. Want 5 points for a quick demo? You'd pad the array with zeros and hope nobody noticed.&lt;/p&gt;
&lt;p&gt;v2 gets rid of that.&lt;/p&gt;
&lt;h4 id=&quot;the-core-change-array-arr-&quot;&gt;The core change: Array (&lt;code&gt;@arr&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;Instead of passing 8 raw numbers into &lt;code&gt;@st-chart-points&lt;/code&gt;, you now declare a named array of any length:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@arr myData[50, 10, 97, 35, 66, 50, 80, 54, 70, 60]

@st-chart-fill(.chart-fill, myData)
@st-chart-line(.chart-line, myData)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;st-core reads the array length at compile time, works out even X-spacing across the chart width, and builds the &lt;code&gt;clip-path: polygon()&lt;/code&gt; shape from however many points you gave it. 5 points, 12 points, 50 points, doesn't matter. No more counting to 8.&lt;/p&gt;
&lt;h4 id=&quot;the-gotcha-that-took-me-a-minute-to-internalize&quot;&gt;The gotcha that took me a minute to internalize&lt;/h4&gt;
&lt;p&gt;Here's the part that actually tripped me up while building the multi-series examples: &lt;code&gt;@st-chart-line(selector, array)&lt;/code&gt; and &lt;code&gt;@st-chart-fill(selector, array)&lt;/code&gt; aren't what set the data on an element. They declare the renderer for that selector, the array argument just tells it how many points to expect.&lt;/p&gt;
&lt;p&gt;The thing that actually writes &lt;code&gt;--st-p1&lt;/code&gt;…&lt;code&gt;--st-p{n}&lt;/code&gt; onto an element is &lt;code&gt;@st-chart-points(array)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That distinction matters the moment you have more than one series on a chart. In a multi-line setup, if &lt;code&gt;.line-b&lt;/code&gt; doesn't call &lt;code&gt;@st-chart-points(seriesB)&lt;/code&gt; itself, it silently inherits whatever the parent container set, meaning it renders the wrong dataset. Looks fine at a glance, wrong shape in practice. Caught it while building a 3-series dashboard card where two lines were quietly drawing the same data.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.fill-b {
  @st-chart-points(seriesB)   /* required, not optional */
  opacity: 0.3;
  --st-accent: #4fffb0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once that clicked, the mental model got simple: renderer declarations are structural (once per selector), point data is per-element (as many times as you have distinct series).&lt;/p&gt;
&lt;h4 id=&quot;how-it-feels-to-ship&quot;&gt;How it feels to ship&lt;/h4&gt;
&lt;p&gt;Pure CSS charting was already a fun constraint to design inside of, no JS runtime, no SVG, no canvas. Making the dataset size flexible removes the last thing that made it feel like a toy rather than something you'd reach for on a real dashboard. Compiled output is still under a kilobyte. Still zero hydration.&lt;/p&gt;
&lt;p&gt;Docs and full examples are up in the repo, including the legacy v1 reference for anyone still on fixed 8-point charts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fscss-ttr/st-core.fscss&quot;&gt;github.com/fscss-ttr/st-core.fscss&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open-source&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;with ♥️&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fscss-ttr/st-core.fscss/blob/main/st-core%40v2.fscss&quot;&gt;source code&lt;/a&gt; is totally math notebook 😅&lt;/p&gt;
&lt;p&gt;Have been writing and debugging from morning till night, and finally released it. The usage is now as simple as CSS.&lt;/p&gt;
&lt;p&gt;😊&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27679/finally-shipped-st-core-fscss-v2-css-charts-without-the-8-point-ceiling</guid>
<pubDate>Sat, 19 Sep 2026 01:08:17 +0000</pubDate>
</item>
<item>
<title>Stop Using Value-Based CSS Variables: Why `--color-action-primary` Beats `--accent-blue`</title>
<link>https://coderlegion.com/26514/stop-using-value-based-css-variables-why-color-action-primary-beats-accent-blue</link>
<description>&lt;p&gt;When setting up CSS custom properties for a new project, it’s tempting to name variables after the values they hold. We’ve all written code like &lt;code&gt;--accent-blue: #3b82f6;&lt;/code&gt; or &lt;code&gt;--dark-gray: #1e293b;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It feels quick and explicit at first. But as soon as your design scales—or as soon as you need to implement dark mode—value-based variable names start working against you.&lt;/p&gt;
&lt;h5 id=&quot;the-problem-with-value-names&quot;&gt;&lt;strong&gt;The Problem with Value Names&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;Value-based names tell you &lt;em&gt;what&lt;/em&gt; a color is, but they don't tell you &lt;em&gt;how&lt;/em&gt; or &lt;em&gt;where&lt;/em&gt; it should be used.&lt;/p&gt;
&lt;p&gt;If &lt;code&gt;--accent-blue&lt;/code&gt; is used for primary buttons, active tab borders, focus rings, and success notifications, what happens when a design update changes the primary button to purple while keeping focus rings blue? You end up creating &lt;code&gt;--accent-blue-2&lt;/code&gt; or &lt;code&gt;--accent-purple&lt;/code&gt;, and your stylesheet grows sideways.&lt;/p&gt;
&lt;p&gt;Even worse, value names completely break down under theme switching:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* Fragile: Value-based naming in Dark Mode */
:root {
  --bg-white: #ffffff;
  --text-black: #0f172a;
}

[data-theme=&quot;dark&quot;] {
  /* Reading &quot;--bg-white&quot; when the hex is dark gray creates instant code confusion */
  --bg-white: #0f172a; 
  --text-black: #f8fafc;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;the-solution-semantic-tokens&quot;&gt;&lt;strong&gt;The Solution: Semantic Tokens&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;Semantic design tokens give variables a &lt;strong&gt;role&lt;/strong&gt; rather than a descriptive name. A semantic token answers the question: &lt;em&gt;&quot;What function does this element perform in the UI?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;When you name custom properties by intent, dark mode and component overrides become effortless:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* Scalable: Role-based Semantic Tokens */
:root {
  /* Primitive Brand Values */
  --blue-600: #2563eb;
  --slate-900: #0f172a;
  --slate-50: #f8fafc;

  /* Semantic UI Tokens (Light Theme) */
  --color-surface-base: var(--slate-50);
  --color-text-main: var(--slate-900);
  --color-action-primary: var(--blue-600);
  --color-border-focus: var(--blue-600);
}

[data-theme=&quot;dark&quot;] {
  /* Dark Theme Overrides map seamlessly without changing variable names */
  --color-surface-base: var(--slate-900);
  --color-text-main: var(--slate-50);
  --color-action-primary: #3b82f6;
  --color-border-focus: #60a5fa;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;how-semantic-tokens-simplify-component-code&quot;&gt;&lt;strong&gt;How Semantic Tokens Simplify Component Code&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;By enforcing semantic tokens, your component styles become immune to global color refactors:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.button-primary {
  background-color: var(--color-action-primary);
  color: var(--color-text-inverse, #ffffff);
}

.button-primary:focus-visible {
  outline: 2px solid var(--color-border-focus);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When your tokens describe &lt;strong&gt;intent&lt;/strong&gt;, changing a color scheme across an entire application takes seconds—and your codebase remains self-documenting for any engineer who reads it.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;call-to-action-cta--1&quot;&gt;&lt;strong&gt;Call to Action (CTA)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;How do you usually structure your CSS variables?&lt;/strong&gt; Do you rely on raw color names or strict semantic tokens when scaling dark mode? Let’s talk in the comments!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(Published by Joseph Salaki — Visual Designer &amp;amp; Frontend Engineer at &lt;a rel=&quot;nofollow&quot; href=&quot;https://byjoemetry.com&quot;&gt;byjoemetry.com&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26514/stop-using-value-based-css-variables-why-color-action-primary-beats-accent-blue</guid>
<pubDate>Mon, 14 Sep 2026 06:00:00 +0000</pubDate>
</item>
<item>
<title>Building Circular UI with circle-progress.fscss (No JavaScript)</title>
<link>https://coderlegion.com/27092/building-circular-ui-with-circle-progress-fscss-no-javascript</link>
<description>&lt;p&gt;Circular progress, pie charts, and step rings usually mean SVG path math or a chart library.&lt;br&gt;
&lt;strong&gt;circle-progress.fscss&lt;/strong&gt; does it with pure CSS — &lt;code&gt;conic-gradient&lt;/code&gt;, masks, and FSCSS &lt;code&gt;@define&lt;/code&gt; helpers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Repo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Figsh/Circle-progress.fscss&quot;&gt;Figsh/Circle-progress.fscss&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/u3yctwx3zjyki8kezc2l.jpg&quot; alt=&quot;Showcase grid — ring, pie, steps, nested&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-you-get&quot;&gt;What you get&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Component &lt;/th&gt;&lt;th&gt;Define&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Progress ring &lt;/td&gt;&lt;td&gt;&lt;code&gt;@circle-progress&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Solid pie &lt;/td&gt;&lt;td&gt;&lt;code&gt;@circle-pie&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Multi-slice pie &lt;/td&gt;&lt;td&gt;&lt;code&gt;@circle-pie-multi&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Steps ring &lt;/td&gt;&lt;td&gt;&lt;code&gt;@circle-steps&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Nested rings &lt;/td&gt;&lt;td&gt;Compose by nesting elements&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;All driven by CSS variables. Change &lt;code&gt;--progress-value&lt;/code&gt; and the arc follows.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;setup-1&quot;&gt;Setup&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Option A — runtime (quick / prototyping)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/fscss@1.2.1/runtime.min.js&quot; defer&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Option B — CLI compile (production)&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;It works well with &lt;a rel=&quot;nofollow&quot; href=&quot;https://marketplace.visualstudio.com/items?itemName=Figsh.fscss&quot;&gt;FSCSS vscode extention&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -g fscss
fscss styles.fscss styles.css
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Import the module:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@import((*) from circle-progress)

@progress-root()
@circle-progress(.progress-circle)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Full sample page:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Figsh/Circle-progress.fscss/blob/main/samples/example.html&quot;&gt;samples/example.html&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-progress-ring&quot;&gt;1. Progress ring&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/xyniobclqzivp7pwc619.jpg&quot; alt=&quot;Green 72% progress ring&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&quot;progress-circle p72&quot;&amp;gt;72%&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.p72 { @progress-range(72) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Under the hood: a &lt;code&gt;conic-gradient&lt;/code&gt; draws the arc, a radial mask cuts the hole, and a small tip marker rotates with the value.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-color-override&quot;&gt;2. Color override&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/kvovpuo4y94ms69x0z71.jpg&quot; alt=&quot;Blue 41% ring&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.p41 {
  @progress-range(41)
  --progress-color-arc: #4d9fff;
  --progress-color-glow: rgba(77, 159, 255, 0.45);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same component — only tokens change.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/f839tpmxd3x6h2guoapq.jpg&quot; alt=&quot;Amber 88% ring&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.p88 {
  @progress-range(88)
  --progress-color-arc: #ffb830;
  --progress-color-glow: rgba(255, 184, 48, 0.4);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-pie-slice&quot;&gt;3. Pie slice&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/mf1n05au58qgg8q12rmo.jpg&quot; alt=&quot;Solid 30% pie&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&quot;circle-pie p30&quot;&amp;gt;30%&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@circle-pie(.circle-pie)
.p30 { @progress-range(30) --progress-color-arc: #ff5070; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No mask — the conic fill is the whole disk.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-multi-pie&quot;&gt;4. Multi pie&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/y5j9mdr3u7kj5s3shbjd.jpg&quot; alt=&quot;4-slice multi pie&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&quot;circle-pie-multi pie-a&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@circle-pie-multi(.circle-pie-multi)

.pie-a {
  --pie-a: 30;
  --pie-b: 25;
  --pie-c: 20;
  --pie-d: 15;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four slices from four percentage tokens.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-steps-ring&quot;&gt;5. Steps ring&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/kf4fdn5b2sku8i69g0gh.jpg&quot; alt=&quot;Steps 5/8&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&quot;circle-steps steps-5&quot;&amp;gt;5/8&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@circle-steps(.circle-steps)
.steps-5 { @steps-done(5) @steps-total(8) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/5wn0qso3ir24p8yls4ka.jpg&quot; alt=&quot;Steps 3/6 violet&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.steps-3 {
  @steps-done(3)
  @steps-total(6)
  --progress-color-arc: #9d7eff;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-nested-rings&quot;&gt;6. Nested rings&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/0t2ioeqg26kwji0w9gta.jpg&quot; alt=&quot;Nested green outer + blue inner&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&quot;progress-circle p72&quot; style=&quot;--progress-size:200px&quot;&amp;gt;
  &amp;lt;div class=&quot;progress-circle p41&quot;
       style=&quot;--progress-size:120px;--progress-stroke:10px&quot;&amp;gt;
    41
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same define, different size/stroke on the child — composition without new APIs.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;mental-model-1&quot;&gt;Mental model&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Tokens&lt;/strong&gt; — &lt;code&gt;@progress-root()&lt;/code&gt; sets size, stroke, colors, value.  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shape&lt;/strong&gt; — one &lt;code&gt;@define&lt;/code&gt; per pattern (ring, pie, steps).  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Value&lt;/strong&gt; — &lt;code&gt;@progress-range(n)&lt;/code&gt; or &lt;code&gt;--progress-value&lt;/code&gt;.  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Theme&lt;/strong&gt; — override &lt;code&gt;--progress-color-arc&lt;/code&gt; / &lt;code&gt;--progress-color-glow&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;No SVG paths. No chart library. Change a variable, the circle updates.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-to-use-runtime-vs-cli&quot;&gt;When to use runtime vs CLI&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Use case &lt;/th&gt;&lt;th&gt;Prefer&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Demos, CodePen, quick tests &lt;/td&gt;&lt;td&gt;CDN &lt;strong&gt;runtime&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Production, static deploy &lt;/td&gt;&lt;td&gt;&lt;strong&gt;CLI&lt;/strong&gt; compile to plain CSS&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Runtime keeps FSCSS syntax in the page. CLI ships zero-runtime CSS.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;try-it-1&quot;&gt;Try it&lt;/h3&gt;
&lt;p&gt;Clone or copy from the repo and open the sample:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Figsh/Circle-progress.fscss&quot;&gt;github.com/Figsh/Circle-progress.fscss&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Sample: &lt;code&gt;samples/example.html&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Swap values, nest rings, add a pie — same module, different tokens.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Built with FSCSS &lt;code&gt;@define&lt;/code&gt; · pure CSS circular UI&lt;/em&gt;&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/27092/building-circular-ui-with-circle-progress-fscss-no-javascript</guid>
<pubDate>Sun, 13 Sep 2026 23:02:30 +0000</pubDate>
</item>
<item>
<title>FSCSS v1.2.0 is out: two clean entry points, full backward compatibility, and inline</title>
<link>https://coderlegion.com/26253/fscss-v1-2-0-is-out-two-clean-entry-points-full-backward-compatibility-and-inline</link>
<description>&lt;p&gt;FSCSS is open source, built in the open, and free for anyone to use, extend, or pick apart. That's the whole point of it. Every release is meant to make it easier for other developers to build with, not harder. v1.2.0 is the clearest example of that yet.&lt;/p&gt;
&lt;h3 id=&quot;what-shipped&quot;&gt;What shipped&lt;/h3&gt;
&lt;p&gt;Previous versions of FSCSS scattered their browser-facing files across the package: &lt;code&gt;/e/exec.js&lt;/code&gt;, &lt;code&gt;/exec.js&lt;/code&gt;, &lt;code&gt;/xfscss.js&lt;/code&gt;, &lt;code&gt;/xfscss.min.js&lt;/code&gt;, and more. Depending on what you needed, you'd end up guessing which file was the right one to load.&lt;/p&gt;
&lt;p&gt;v1.2.0 consolidates all of that into two public entry points:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- Auto-run, for plain HTML pages --&amp;gt;
&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/fscss@1.2.0/runtime.min.js&quot; async&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Manual control, for tooling and modules
import xfscss from &quot;https://cdn.jsdelivr.net/npm/fscss@1.2.0/esm.js&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;runtime.js&lt;/code&gt; auto-scans the page and compiles everything on load, &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; blocks, &lt;code&gt;&amp;lt;link type=&quot;fscss&quot;&amp;gt;&lt;/code&gt; sources, and now inline &lt;code&gt;style&lt;/code&gt; attributes too. &lt;code&gt;esm.js&lt;/code&gt; never auto-runs and never touches page loading, since it's meant for using FSCSS as a tool: an FSCSS-to-CSS converter, a build step, or any pipeline where you decide what gets processed and when.&lt;/p&gt;
&lt;p&gt;Use one or the other. Never both.&lt;/p&gt;
&lt;h3 id=&quot;nothing-breaks&quot;&gt;Nothing breaks&lt;/h3&gt;
&lt;p&gt;This is the part worth repeating: the npm package and CLI are completely unchanged, and every method that worked in the 1.1.x line still works exactly the same way in 1.2.0. &lt;code&gt;@define&lt;/code&gt;, &lt;code&gt;@fun&lt;/code&gt;, &lt;code&gt;@event&lt;/code&gt;, &lt;code&gt;@arr&lt;/code&gt;, &lt;code&gt;pattern()&lt;/code&gt;, all of it. The consolidation only touches the browser entry-point files. If you're compiling ahead of time with the CLI, this release changes nothing for you.&lt;/p&gt;
&lt;h3 id=&quot;inline-styles-now-go-through-the-full-pipeline&quot;&gt;Inline styles now go through the full pipeline&lt;/h3&gt;
&lt;p&gt;This is the headline feature. From v1.2.0, &lt;code&gt;style=&quot;...&quot;&lt;/code&gt; attributes are compiled on boot the same way &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; blocks are:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;style&amp;gt;
  @define btn-style(bg: #636EE7){
    padding: 10px 20px;
    color: color-mix(#dddddd, @use(bg));
    background: @use(bg);
    border: 2px solid;
    border-radius: 25px;
    font-weight: 700;
  }
&amp;lt;/style&amp;gt;

&amp;lt;button style=&quot;@btn-style(#1a2a4f)&quot;&amp;gt;Search&amp;lt;/button&amp;gt;
&amp;lt;button style=&quot;@btn-style(#f00f00)&quot;&amp;gt;Search&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/7y11c39cjxmto7kaj3b8.jpg&quot; alt=&quot;FSCSS output&quot;&gt;&lt;/p&gt;
&lt;p&gt;Combined with &lt;code&gt;xfscss.process()&lt;/code&gt;, that means an inline mixin call can be recompiled at runtime and swapped straight back into the element, no rewritten CSS, no framework state:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const update = async (btn) =&amp;gt; {
  const newStyle = await xfscss.process(&quot;@btn-style(@arr.colors!.randint)&quot;);
  btn.setAttribute(&quot;style&quot;, newStyle);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The full walkthrough of this: including a detail (method declared inline stay resolvable even after their declaration is cleaned out of the attribute), if you want to go deeper: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/figsh/fscss-v120-lets-you-write-mixins-directly-in-style-heres-why-thats-a-bigger-deal-than-2a31&quot;&gt;FSCSS v1.2.0 lets you write mixins directly in style=&quot;...&quot;&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;why-this-matters-for-an-open-source-tool&quot;&gt;Why this matters for an open-source tool&lt;/h3&gt;
&lt;p&gt;None of this is about adding complexity for its own sake. FSCSS exists to cut down on repetitive CSS and give developers capabilities plain CSS doesn't have natively, and every release should make that easier to reach for, not harder to keep up with. Two entry points instead of a pile of internal files. Full compatibility instead of a migration guide. A feature that makes the JS API and the language work together instead of sitting side by side.&lt;/p&gt;
&lt;p&gt;Open source only stays useful if the people using it can trust an upgrade won't cost them a rewrite. That's the bar this release was held to.&lt;/p&gt;
&lt;h3 id=&quot;try-it-2&quot;&gt;try it&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install fscss@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/fscss@1.2.0/runtime.min.js&quot; async&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Vscode: &lt;a rel=&quot;nofollow&quot; href=&quot;https://marketplace.visualstudio.com/items?itemName=Figsh.fscss&quot;&gt;FSCSS support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;API reference: &lt;a rel=&quot;nofollow&quot; href=&quot;https://fscss.devtem.org/api&quot;&gt;fscss.devtem.org/api&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Figsh/xfscss&quot;&gt;github.com/Figsh/xfscss&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;NPM: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/fscss&quot;&gt;npmjs.com/package/fscss&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bug reports, feature requests, and pull requests are all welcome on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Figsh/xfscss&quot;&gt;GitHub&lt;/a&gt; &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26253/fscss-v1-2-0-is-out-two-clean-entry-points-full-backward-compatibility-and-inline</guid>
<pubDate>Sat, 05 Sep 2026 08:26:35 +0000</pubDate>
</item>
<item>
<title>The bug nobody fixed for 4 years (and how it got me my first Contributor badge)</title>
<link>https://coderlegion.com/26039/the-bug-nobody-fixed-for-4-years-and-how-it-got-me-my-first-contributor-badge</link>
<description>&lt;p&gt;Have you ever opened the issues tab of an open source project, stared at the list, and closed it?&lt;/p&gt;
&lt;p&gt;I have. More than once.&lt;/p&gt;
&lt;p&gt;The feeling is always the same: &quot;this is for grown-ups, I'm not good enough to touch it&quot;. So you close the tab, go back to your side project, and contributing becomes something you'll do when you're &quot;ready&quot;.&lt;/p&gt;
&lt;p&gt;Spoiler: you'll never be ready. I started by fixing a footer that broke on mobile.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-project-and-the-bug&quot;&gt;The project and the bug&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/aihqqrrezhmmge6lun92.png&quot; alt=&quot;TimeConv Banner&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ArjunSharda/TimeConv&quot;&gt;TimeConv&lt;/a&gt; is a simple time converter: you give it a date and it hands you back UTC, seconds, or whatever timezone you asked for. Static site, hand-written HTML and CSS, no framework.&lt;/p&gt;
&lt;p&gt;Issue &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ArjunSharda/TimeConv/issues/41&quot;&gt;#41&lt;/a&gt; said the footer broke on mobile. Opened in &lt;strong&gt;August 2022&lt;/strong&gt;. Four people passed through it over the years, diagnosed the problem, agreed with the diagnosis...&lt;/p&gt;
&lt;p&gt;And not one of them opened a PR.&lt;/p&gt;
&lt;p&gt;That's the kind of issue I recommend if you're starting out: old, already diagnosed by other people, and with nobody working on it. Half the work is done and the odds of duplicating someone's effort are low.&lt;/p&gt;
&lt;h3 id=&quot;the-css-bug-that-wasn-t-a-css-bug&quot;&gt;The CSS bug that wasn't a CSS bug&lt;/h3&gt;
&lt;p&gt;The original code did this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;footer {
  position: absolute;
  height: 15vh;
  bottom: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Absolutely positioned footer, pinned to the bottom. Works beautifully — until the page has enough content to scroll. Then the footer floats in the middle of your text, because &lt;code&gt;bottom: 0&lt;/code&gt; is relative to the viewport, not to the content.&lt;/p&gt;
&lt;p&gt;The modern fix is the flexbox sticky footer: make &lt;code&gt;body&lt;/code&gt; a flex column with &lt;code&gt;min-height: 100vh&lt;/code&gt;, and give the footer &lt;code&gt;margin-top: auto&lt;/code&gt;, which pushes it into whatever space is left over.&lt;/p&gt;
&lt;p&gt;I swapped it, tested it, and... two pages didn't budge.&lt;/p&gt;
&lt;p&gt;Here's the part I didn't see coming, and the most useful thing I took from this contribution: &lt;strong&gt;&lt;code&gt;margin-top: auto&lt;/code&gt; only pushes an element if it's a direct child of the flex container.&lt;/strong&gt; In &lt;code&gt;credits.html&lt;/code&gt; the footer sat four levels deep inside a &lt;code&gt;.row&lt;/code&gt; — which is a horizontal flex container. And in &lt;code&gt;utcsecondstodatetime.html&lt;/code&gt; it was literally outside &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So I opened the HTML of those two files and found a duplicated &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;, orphan &lt;code&gt;&amp;lt;/div&amp;gt;&lt;/code&gt;s, and tags closing out of order. The file was broken badly enough that &lt;code&gt;prettier&lt;/code&gt; couldn't even parse it.&lt;/p&gt;
&lt;p&gt;Which means: the issue was filed as a CSS bug, and no amount of CSS was ever going to fix it without touching the HTML. That's why four people diagnosed it and nobody shipped — the diagnosis was only half right.&lt;/p&gt;
&lt;p&gt;Final PR: &lt;strong&gt;7 files, 175 lines added, 174 removed.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/hxwycp0cvrxwy3ms7rjy.png&quot; alt=&quot;github PR&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-red-check-that-almost-killed-it&quot;&gt;The red check that almost killed it&lt;/h3&gt;
&lt;p&gt;PR opened, and CI lights up: &lt;code&gt;Lint Code Base&lt;/code&gt; failed.&lt;/p&gt;
&lt;p&gt;The beginner reflex (mine included) is to run &lt;code&gt;prettier --write&lt;/code&gt; across the repo and push everything green. Don't. That would have added ~138 lines of reindentation to a 7-file PR, and the reviewer would have had to separate the fix from the formatting.&lt;/p&gt;
&lt;p&gt;What I did instead was pull the original files from &lt;code&gt;main&lt;/code&gt; and run the linter against them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx prettier --check docs/
npx htmlhint docs/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Result: the &lt;strong&gt;original&lt;/strong&gt; files already failed. And &lt;code&gt;utcsecondstodatetime.html&lt;/code&gt; on &lt;code&gt;main&lt;/code&gt; didn't even parse. htmlhint reported 18 errors on the original versus 0 on mine.&lt;/p&gt;
&lt;p&gt;I posted that in the PR, with the output from both sides. That is precisely what unblocked the merge — the maintainer could see the red was inherited instead of taking my word for it.&lt;/p&gt;
&lt;p&gt;It's a rule for me now:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;An inherited red check gets explained with reproduced evidence, not fixed by growing the PR.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Three weeks after opening it (and one polite ping along the way, because maintainers of small projects do go quiet), I got an &quot;Awesome, LGTM!&quot; and the merge.&lt;/p&gt;
&lt;h3 id=&quot;hands-on-test-a-static-site-in-30-seconds&quot;&gt;Hands on: test a static site in 30 seconds&lt;/h3&gt;
&lt;p&gt;No Node, no build step, nothing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/ArjunSharda/TimeConv.git
cd TimeConv/docs
python3 -m http.server 8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;http://localhost:8000&lt;/code&gt;, hit F12, turn on the device toolbar and test at 320, 375 and 768 pixels wide. Test one short page and one long page — a footer bug only shows up in one of them.&lt;/p&gt;
&lt;p&gt;That's the entire test loop for a static-site contribution. Really.&lt;/p&gt;
&lt;h3 id=&quot;-but-that-s-just-css-it-doesn-t-count-&quot;&gt;&quot;But that's just CSS, it doesn't count&quot;&lt;/h3&gt;
&lt;p&gt;It counts. And let me lay out both sides, because this choice is yours.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Small project.&lt;/strong&gt; Upside: the maintainer actually reads your PR, the codebase fits in your head, and you get to close the full issue → PR → merge loop, which is where the learning lives. Downside: little reach, sometimes an inactive maintainer, and a process too informal to teach you rigor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Large project.&lt;/strong&gt; Upside: serious CI, hard review, documented conventions — you learn the craft. Downside: long review queues, and a real chance someone fixes the bug before you do (that happened to me on another project, and it's an article of its own).&lt;/p&gt;
&lt;p&gt;I don't think there's a right side. I think starting small and moving up is kinder to your motivation.&lt;/p&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;
&lt;p&gt;Seven files, 175 lines added, zero lines of complex logic. Result: a Contributor badge and a four-year-old issue closed.&lt;/p&gt;
&lt;p&gt;What was blocking that issue wasn't technical difficulty. It was that nobody had opened the HTML.&lt;/p&gt;
&lt;p&gt;Your turn: open the issues tab of a project you actually use, filter by &lt;code&gt;is:issue is:open sort:created-asc&lt;/code&gt;, and look at the oldest ones. One of them is waiting for somebody to open the file.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/gh55l9eotfc8z1vihc1j.png&quot; alt=&quot;Github query&quot;&gt;&lt;/p&gt;
&lt;p&gt;And if you do it, brace yourself for one thing: &lt;strong&gt;the merge isn't the end.&lt;/strong&gt; After mine landed I got excited and kept poking around the site — and while testing the pages in the browser, I noticed the converters only worked once. A second conversion needed a page reload.&lt;/p&gt;
&lt;p&gt;There were two bugs. They hid each other, with a side effect that pushed whatever the user typed straight into their browser history.&lt;/p&gt;
&lt;p&gt;That's the next article.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Author's note:&lt;/strong&gt; this was my first contribution to TimeConv, merged in August 2026. I'm documenting every open source contribution I make in this series. If you contribute to open source too, I'd like to hear about it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ArjunSharda/TimeConv&quot;&gt;TimeConv on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ArjunSharda/TimeConv/issues/41&quot;&gt;Issue #41 — Website is not responsive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ArjunSharda/TimeConv/pull/63&quot;&gt;PR #63 — make footer sticky and layout responsive on mobile&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout/Basic_concepts_of_flexbox&quot;&gt;MDN — Basic concepts of flexbox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://css-tricks.com/couple-takes-sticky-footer/&quot;&gt;CSS Tricks — Sticky footer, five ways&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26039/the-bug-nobody-fixed-for-4-years-and-how-it-got-me-my-first-contributor-badge</guid>
<pubDate>Tue, 01 Sep 2026 23:02:15 +0000</pubDate>
</item>
<item>
<title>Building Fluentic Style: Bringing React Native Style Composition to React</title>
<link>https://coderlegion.com/25725/building-fluentic-style-bringing-react-native-style-composition-to-react</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://fluenticstack.com/style&quot;&gt;Fluentic Style&lt;/a&gt; is my attempt to make React styling feel explicit, composable, and predictable again, without giving up the ergonomics developers expect.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Years ago, I worked on web apps with Emotion, and I loved it.&lt;/p&gt;
&lt;p&gt;It was flexible, productive, and convenient. But over time, some of the codebases I worked on became harder and harder to change. I do not blame Emotion for that. The problem was how I used the convenience it gave me.&lt;/p&gt;
&lt;p&gt;I wrote class names that depended on DOM structure. I used nested selectors because they were quick. I reached into child elements from parent styles. It worked, until months later I had to change something and no longer felt confident about what else would break.&lt;/p&gt;
&lt;p&gt;That feeling is what I remember most.&lt;/p&gt;
&lt;p&gt;Not that the styling was impossible. Not that the tool was bad. Just that I slowly lost confidence when changing UI.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-was-confidence&quot;&gt;The Problem Was Confidence&lt;/h3&gt;
&lt;p&gt;A lot of frontend styling problems are not about whether something is technically possible.&lt;/p&gt;
&lt;p&gt;CSS is powerful. CSS-in-JS is powerful. Utility classes are powerful. CSS Modules are useful. Static extraction is useful.&lt;/p&gt;
&lt;p&gt;The hard part is usually confidence.&lt;/p&gt;
&lt;p&gt;Can I change this component without accidentally changing another component?&lt;/p&gt;
&lt;p&gt;Can I understand where this style comes from?&lt;/p&gt;
&lt;p&gt;Can I tell which style wins?&lt;/p&gt;
&lt;p&gt;Can I move this element without breaking some selector far away?&lt;/p&gt;
&lt;p&gt;Can I delete this class name?&lt;/p&gt;
&lt;p&gt;Can I add a variant without making the component more fragile?&lt;/p&gt;
&lt;p&gt;When styles depend on class names, DOM structure, nested selectors, and implicit relationships between elements, every change becomes a little less obvious.&lt;/p&gt;
&lt;p&gt;You can still build good systems this way. Many teams do.&lt;/p&gt;
&lt;p&gt;But that way of styling makes it very easy to create hidden coupling.&lt;/p&gt;
&lt;p&gt;And hidden coupling is where confidence starts to fade.&lt;/p&gt;
&lt;h3 id=&quot;then-i-spent-years-in-react-native&quot;&gt;Then I Spent Years in React Native&lt;/h3&gt;
&lt;p&gt;After that, I spent years mostly working on React Native projects.&lt;/p&gt;
&lt;p&gt;React Native styling changed how I think about UI.&lt;/p&gt;
&lt;p&gt;The styling model is much smaller:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;View style={[styles.root, disabled &amp;amp;&amp;amp; styles.disabled, style]} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Styles are objects.&lt;/p&gt;
&lt;p&gt;Styles are passed as values.&lt;/p&gt;
&lt;p&gt;Arrays compose styles.&lt;/p&gt;
&lt;p&gt;The later style wins.&lt;/p&gt;
&lt;p&gt;Variants are usually explicit.&lt;/p&gt;
&lt;p&gt;The style belongs close to the element that uses it.&lt;/p&gt;
&lt;p&gt;There is no class name pretending to be both identity and styling hook. There is no global selector reaching through some DOM structure. There is much less “where did this come from?”&lt;/p&gt;
&lt;p&gt;Of course React Native styling is not perfect. It has its own limitations. But the mental model is very simple.&lt;/p&gt;
&lt;p&gt;And after using that model for years, I came back to React web.&lt;/p&gt;
&lt;p&gt;That was when web styling felt more complicated to me than it used to.&lt;/p&gt;
&lt;p&gt;Not because CSS got worse.&lt;/p&gt;
&lt;p&gt;Actually, CSS got much better.&lt;/p&gt;
&lt;p&gt;We have better layout. Better selectors. Better browser support. Better build tools. Better frameworks.&lt;/p&gt;
&lt;p&gt;But for component-heavy React work, I started to miss the React Native mental model a lot.&lt;/p&gt;
&lt;p&gt;In React Native, this kind of style composition is completely normal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;View style={[base, variant, state, props.style]} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that normality matters.&lt;/p&gt;
&lt;p&gt;Base first. Then the variant. Then the current state. Then the style passed from outside.&lt;/p&gt;
&lt;p&gt;It makes styling feel like data. Like values. Like composition.&lt;/p&gt;
&lt;p&gt;Not like a string contract with the DOM.&lt;/p&gt;
&lt;p&gt;React web already has pieces of this too. Emotion’s &lt;code&gt;css&lt;/code&gt; prop can feel especially close, and other tools have their own ways to compose styles.&lt;/p&gt;
&lt;p&gt;However the thing I wanted was not just style composition for one element.&lt;/p&gt;
&lt;p&gt;React Native does not solve every part of this either. When a component has multiple internal elements, you still need to decide which parts can be styled and how those styles are passed through.&lt;/p&gt;
&lt;p&gt;I wanted the same explicit composition to work not only for one root element, but across a whole styleable component: root, icon, label, header, content, footer, or whatever parts the component chooses to expose.&lt;/p&gt;
&lt;p&gt;Not just root element composition:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;Component css={[base, variant, props.css]} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But a component style theme where each public part can be composed explicitly, without the caller reaching into DOM structure or relying on class names as hidden hooks, with something as simple as this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;Component theme={[baseTheme, variantTheme, props.theme]} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is where Fluentic started to feel different.&lt;/p&gt;
&lt;h3 id=&quot;react-web-has-something-react-native-misses&quot;&gt;React Web Has Something React Native Misses&lt;/h3&gt;
&lt;p&gt;But I do not want to pretend React Native has the perfect model.&lt;/p&gt;
&lt;p&gt;It does not.&lt;/p&gt;
&lt;p&gt;The web has something extremely useful: selectors.&lt;/p&gt;
&lt;p&gt;Sometimes selectors are exactly what you want.&lt;/p&gt;
&lt;p&gt;You want hover states:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.button:hover {
  background: var(--hover);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You want focus states.&lt;/p&gt;
&lt;p&gt;You want child coordination.&lt;/p&gt;
&lt;p&gt;You want media queries.&lt;/p&gt;
&lt;p&gt;You want pseudo elements.&lt;/p&gt;
&lt;p&gt;You want to style based on parent state.&lt;/p&gt;
&lt;p&gt;You want to express relationships without manually passing a style prop into every single element.&lt;/p&gt;
&lt;p&gt;That is real power.&lt;/p&gt;
&lt;p&gt;React Native often makes this more manual. You wire state. You pass style props. You conditionally apply styles element by element.&lt;/p&gt;
&lt;p&gt;That explicitness is nice until it becomes repetitive.&lt;/p&gt;
&lt;p&gt;So the question I kept thinking about was:&lt;/p&gt;
&lt;p&gt;Can React styling keep the simple React Native mental model, but still keep the useful parts of CSS?&lt;/p&gt;
&lt;p&gt;Can styles be values, while selectors still exist?&lt;/p&gt;
&lt;p&gt;Can composition be explicit, while output is still real CSS?&lt;/p&gt;
&lt;p&gt;Can override order be obvious, while runtime work stays small?&lt;/p&gt;
&lt;p&gt;That question became the beginning of Fluentic.&lt;/p&gt;
&lt;h3 id=&quot;extraction-is-not-the-new-idea&quot;&gt;Extraction Is Not the New Idea&lt;/h3&gt;
&lt;p&gt;Modern React styling has changed a lot.&lt;/p&gt;
&lt;p&gt;Runtime CSS-in-JS used to feel like a reasonable default for many React apps. Today, the tradeoffs are more visible.&lt;/p&gt;
&lt;p&gt;Performance matters.&lt;/p&gt;
&lt;p&gt;Server rendering matters.&lt;/p&gt;
&lt;p&gt;Streaming matters.&lt;/p&gt;
&lt;p&gt;React Server Components changed what can happen on the server and what should happen on the client.&lt;/p&gt;
&lt;p&gt;Build-time extraction is no longer a niche idea. In many modern stacks, it feels close to mandatory.&lt;/p&gt;
&lt;p&gt;But extraction itself is not the interesting part anymore.&lt;/p&gt;
&lt;p&gt;There are already many libraries that extract CSS.&lt;/p&gt;
&lt;p&gt;That is not the main reason I started working on Fluentic.&lt;/p&gt;
&lt;p&gt;The thing I felt missing was not “another extracted CSS-in-JS library”.&lt;/p&gt;
&lt;p&gt;The thing I wanted was a different way to write and compose styles.&lt;/p&gt;
&lt;p&gt;A model closer to what I had in my head after years of React Native:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;styles as values&lt;/li&gt;
&lt;li&gt;explicit composition&lt;/li&gt;
&lt;li&gt;predictable override order&lt;/li&gt;
&lt;li&gt;local ownership&lt;/li&gt;
&lt;li&gt;variants without magic class names&lt;/li&gt;
&lt;li&gt;selectors when useful, but not as the default way to connect everything&lt;/li&gt;
&lt;li&gt;build-time output that fits modern React apps&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the shape of Fluentic.&lt;/p&gt;
&lt;h3 id=&quot;what-i-want-from-a-styling-system&quot;&gt;What I Want From a Styling System&lt;/h3&gt;
&lt;p&gt;When I started thinking about Fluentic, I wrote down the requirements more like a frontend developer who has been hurt by his own past decisions than like someone trying to invent a styling library.&lt;/p&gt;
&lt;p&gt;The requirements were not only technical.&lt;/p&gt;
&lt;p&gt;They were emotional too.&lt;/p&gt;
&lt;p&gt;I wanted to feel confident changing UI again.&lt;/p&gt;
&lt;h4 id=&quot;1-styles-should-be-values&quot;&gt;1. Styles Should Be Values&lt;/h4&gt;
&lt;p&gt;In React Native, a style is something you can pass around.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;View style={styles.card} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or compose:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;View style={[styles.card, elevated &amp;amp;&amp;amp; styles.elevated]} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That feels very different from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;div className={`${styles.card} ${elevated ? styles.elevated : &quot;&quot;}`} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The class name version works, of course. But the mental model is different.&lt;/p&gt;
&lt;p&gt;One feels like composing values.&lt;/p&gt;
&lt;p&gt;The other feels like constructing a string that points to styling somewhere else.&lt;/p&gt;
&lt;p&gt;In Fluentic, the equivalent idea uses &lt;code&gt;style(...)&lt;/code&gt; and the JSX &lt;code&gt;css&lt;/code&gt; prop:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { style } from '@fluentic/style';

const styles = {
  card: style({
    padding: 16,
    borderRadius: 8,
    backgroundColor: 'white',
  }),

  elevated: style({
    boxShadow: '0 12px 30px rgb(15 23 42 / 0.16)',
  }),
};

function Card({ elevated }: { elevated?: boolean }) {
  return (
    &amp;lt;section css={[styles.card, elevated &amp;amp;&amp;amp; styles.elevated]}&amp;gt;
      Ready to style.
    &amp;lt;/section&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The syntax is different from React Native, because the target is still the browser.&lt;/p&gt;
&lt;p&gt;But the feeling is familiar:&lt;/p&gt;
&lt;p&gt;Base style first.&lt;/p&gt;
&lt;p&gt;Conditional style after.&lt;/p&gt;
&lt;p&gt;The style value is passed directly to the element.&lt;/p&gt;
&lt;h4 id=&quot;2-override-order-should-be-obvious&quot;&gt;2. Override Order Should Be Obvious&lt;/h4&gt;
&lt;p&gt;One of the nicest things about React Native style arrays is that the later item wins.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;View style={[styles.base, styles.large, props.style]} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can read that line and understand the override order.&lt;/p&gt;
&lt;p&gt;Base first.&lt;br&gt;
Then large.&lt;br&gt;
Then external style.&lt;/p&gt;
&lt;p&gt;There is very little mystery.&lt;/p&gt;
&lt;p&gt;On the web, override order can come from many places:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;stylesheet order&lt;/li&gt;
&lt;li&gt;selector specificity&lt;/li&gt;
&lt;li&gt;class order&lt;/li&gt;
&lt;li&gt;nested selectors&lt;/li&gt;
&lt;li&gt;cascade layers&lt;/li&gt;
&lt;li&gt;inline styles&lt;/li&gt;
&lt;li&gt;generated CSS order&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some of these are powerful. Some are necessary. But when building components, I often want the boring rule:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;later style wins&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Fluentic is designed around that kind of predictability.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;button css={[styles.button, primary &amp;amp;&amp;amp; styles.primary, props.css]}&amp;gt;
  Save
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the kind of composition I want to keep visible in React code.&lt;/p&gt;
&lt;h4 id=&quot;3-variants-should-be-explicit&quot;&gt;3. Variants Should Be Explicit&lt;/h4&gt;
&lt;p&gt;I have written many components where styles were controlled by a mix of class names and nested selectors.&lt;/p&gt;
&lt;p&gt;Something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.card.compact .title {
  font-size: 14px;
}

.card.highlighted .title {
  color: blue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is convenient.&lt;/p&gt;
&lt;p&gt;It also couples the parent state, child structure, and class names together.&lt;/p&gt;
&lt;p&gt;Later, when the component changes, the style assumptions are still sitting there.&lt;/p&gt;
&lt;p&gt;For a local component, explicit composition can be enough:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const styles = {
  card: style({
    padding: 16,
    borderRadius: 8,
    backgroundColor: 'white',
  }),

  compactCard: style({
    padding: 12,
  }),

  title: style({
    fontSize: 20,
    fontWeight: 700,
  }),

  compactTitle: style({
    fontSize: 16,
  }),
};

function Card({ compact }: { compact?: boolean }) {
  return (
    &amp;lt;div css={[styles.card, compact &amp;amp;&amp;amp; styles.compactCard]}&amp;gt;
      &amp;lt;h2 css={[styles.title, compact &amp;amp;&amp;amp; styles.compactTitle]}&amp;gt;
        Title
      &amp;lt;/h2&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This may look less magical.&lt;/p&gt;
&lt;p&gt;That is the point.&lt;/p&gt;
&lt;p&gt;A little less magic often means a lot more confidence later.&lt;/p&gt;
&lt;h4 id=&quot;4-reusable-components-need-more-than-root-styling&quot;&gt;4. Reusable Components Need More Than Root Styling&lt;/h4&gt;
&lt;p&gt;Root-level styling is only the small case.&lt;/p&gt;
&lt;p&gt;Reusable React components often have internal parts.&lt;/p&gt;
&lt;p&gt;A button may have a root, an icon, and a label.&lt;/p&gt;
&lt;p&gt;A card may have a root, header, title, content, and footer.&lt;/p&gt;
&lt;p&gt;A caller might need to customize those parts without depending on DOM structure.&lt;/p&gt;
&lt;p&gt;With normal class names, the public styling API often becomes accidental:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;button className=&quot;button&quot;&amp;gt;
  &amp;lt;span className=&quot;button__icon&quot; /&amp;gt;
  &amp;lt;span className=&quot;button__label&quot;&amp;gt;Save&amp;lt;/span&amp;gt;
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That works.&lt;/p&gt;
&lt;p&gt;But now the caller knows your class names.&lt;/p&gt;
&lt;p&gt;And if they use selectors, they may also start to know your markup.&lt;/p&gt;
&lt;p&gt;Fluentic uses slots for this.&lt;/p&gt;
&lt;p&gt;A slot is a named styling target that a component chooses to expose:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { style } from '@fluentic/style';

const buttonStyles = {
  root: style.slot({
    display: 'inline-flex',
    alignItems: 'center',
    gap: 8,
    border: 0,
    borderRadius: 8,
    padding: '8px 12px',
  }),

  icon: style.slot({
    display: 'inline-flex',
    width: 16,
    height: 16,
  }),

  label: style.slot({
    fontWeight: 650,
  }),
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This says something important:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;root&lt;/code&gt;, &lt;code&gt;icon&lt;/code&gt;, and &lt;code&gt;label&lt;/code&gt; are allowed styling targets.&lt;/p&gt;
&lt;p&gt;The DOM can stay private.&lt;/p&gt;
&lt;p&gt;The component decides what it exposes.&lt;/p&gt;
&lt;h4 id=&quot;5-selectors-should-exist-but-they-should-not-become-the-whole-model&quot;&gt;5. Selectors Should Exist, But They Should Not Become the Whole Model&lt;/h4&gt;
&lt;p&gt;I still want hover and focus styles to feel natural.&lt;/p&gt;
&lt;p&gt;In Fluentic, a simple element style can use fluent selector/state chains:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const button = style({
  backgroundColor: 'white',
  border: '1px solid var(--color-border)',
}).hover({
  backgroundColor: 'var(--color-surface-hover)',
}).focusVisible({
  outline: '2px solid var(--color-focus)',
  outlineOffset: 2,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hover and focus states belong naturally in CSS.&lt;/p&gt;
&lt;p&gt;I do not want to manually wire every interaction state in JavaScript just to avoid selectors.&lt;/p&gt;
&lt;p&gt;But I also do not want selectors to become the main way components talk to each other.&lt;/p&gt;
&lt;p&gt;Selectors are useful.&lt;/p&gt;
&lt;p&gt;Selectors are also where hidden relationships often begin.&lt;/p&gt;
&lt;p&gt;So Fluentic should support selectors, but still encourage local, explicit style ownership.&lt;/p&gt;
&lt;h4 id=&quot;6-runtime-work-should-be-small&quot;&gt;6. Runtime Work Should Be Small&lt;/h4&gt;
&lt;p&gt;Modern React apps already have enough work to do at runtime.&lt;/p&gt;
&lt;p&gt;Styling should not require unnecessary client-side computation when the result can be known at build time.&lt;/p&gt;
&lt;p&gt;This matters even more with SSR and React Server Components.&lt;/p&gt;
&lt;p&gt;So Fluentic needs extraction.&lt;/p&gt;
&lt;p&gt;But again, extraction is the implementation requirement, not the whole product idea.&lt;/p&gt;
&lt;p&gt;The product idea is the way styles are written and composed.&lt;/p&gt;
&lt;h4 id=&quot;7-debugging-should-lead-back-to-the-code&quot;&gt;7. Debugging Should Lead Back to the Code&lt;/h4&gt;
&lt;p&gt;This does not happen every day, but when it happens, it is painful.&lt;/p&gt;
&lt;p&gt;I have had simple style issues take much longer than they should because tracing the final rule back to the code was not clear enough. Sometimes the source map was not precise. Sometimes the class name pointed me to generated output, but not to the exact style code that produced it.&lt;/p&gt;
&lt;p&gt;That kind of debugging makes me lose confidence quickly.&lt;/p&gt;
&lt;p&gt;Atomic CSS has a real benefit: it can keep the final CSS output small. But it also means the browser often shows many generated class names in the HTML. That tradeoff is acceptable only if those class names can still be traced back to the style code that created them.&lt;/p&gt;
&lt;p&gt;If the browser shows an atomic class name, I still need a way to answer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;which code created this rule?&lt;/li&gt;
&lt;li&gt;which component owns it?&lt;/li&gt;
&lt;li&gt;did this come from the base style, a variant, a scope, or an incoming override?&lt;/li&gt;
&lt;li&gt;why did this style win?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because styling bugs are often not about one declaration. They are about the path that declaration took into the UI.&lt;/p&gt;
&lt;p&gt;So Fluentic is not only about producing CSS. It is also about making generated styles easier to trace back to the React code that created them.&lt;/p&gt;
&lt;h4 id=&quot;8-app-themes-should-not-be-an-afterthought&quot;&gt;8. App Themes Should Not Be an Afterthought&lt;/h4&gt;
&lt;p&gt;There are two things many apps do not need on day one, but often wish they had prepared for from the beginning: i18n and theming.&lt;/p&gt;
&lt;p&gt;At first, it is easy to assume the app only needs one language, one brand, one density, and one visual mode.&lt;/p&gt;
&lt;p&gt;Then later the app needs dark mode. Or a different brand theme. Or a denser admin layout. Or product-specific styling.&lt;/p&gt;
&lt;p&gt;That is when theming becomes painful.&lt;/p&gt;
&lt;p&gt;Not because dark mode is hard by itself, but because the codebase was not prepared for styles to change at the app level.&lt;/p&gt;
&lt;p&gt;So Fluentic has to support theming without forcing every value to become a carefully named token from day one.&lt;/p&gt;
&lt;p&gt;Named tokens are useful when the name matters, which is usually when a component exposes values that other code is expected to override intentionally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const card = createTokens({
  surface: '#ffffff',
  text: '#111827',
  border: '#d8e3df',
});

const cardStyles = {
  root: style.slot({
    backgroundColor: card.surface,
    color: card.text,
    border: '1px solid',
    borderColor: card.border,
  }),
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the component has a small theme surface.&lt;/p&gt;
&lt;p&gt;Other code does not need to know its DOM structure. It only needs to know the values the component chose to expose.&lt;/p&gt;
&lt;p&gt;But at the app level, naming every color or spacing value can become more effort than help.&lt;/p&gt;
&lt;p&gt;Sometimes the value itself is the clearest thing to read, search, and maintain.&lt;/p&gt;
&lt;p&gt;That is why Fluentic also has &lt;code&gt;createValues(...)&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const color = createValues([
  '#ffffff | Surface',
  '#111827 | Text',
  '#d8e3df | Border',
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The app can use those values directly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const page = style({
  backgroundColor: color('#ffffff | Surface'),
  color: color('#111827 | Text'),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And &lt;code&gt;createTheme(...)&lt;/code&gt; can provide different values at the app or subtree level:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const darkTheme = createTheme([
  color('#ffffff | Surface', '#111827'),
  color('#111827 | Text', '#ffffff'),
  color('#d8e3df | Border', '#374151'),
]);

function App() {
  return (
    &amp;lt;main css={[page, darkTheme]}&amp;gt;
      &amp;lt;Dashboard /&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the component level, those app values can be mapped into the component’s exposed tokens through &lt;code&gt;style.scope(...)&lt;/code&gt; and passed to the component:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const dashboardCardTheme = style.scope([
  card.surface(color('#ffffff | Surface')),
  card.text(color('#111827 | Text')),
  card.border(color('#d8e3df | Border')),
]);

&amp;lt;Card theme={dashboardCardTheme} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The app theme still controls what those app values become. The component theme only says how this component should connect to those values.&lt;/p&gt;
&lt;p&gt;The goal is not to force every app to define many themes from day one.&lt;/p&gt;
&lt;p&gt;The goal is to make themeability available from the beginning, so dark mode and app-level styling do not become a retrofit later.&lt;/p&gt;
&lt;h3 id=&quot;what-fluentic-is-trying-to-be&quot;&gt;What Fluentic Is Trying To Be&lt;/h3&gt;
&lt;p&gt;Fluentic is my attempt to bring a React Native-inspired styling mental model to React web apps.&lt;/p&gt;
&lt;p&gt;The goal is not to hide CSS.&lt;/p&gt;
&lt;p&gt;The web is still the web.&lt;/p&gt;
&lt;p&gt;You still need to understand layout, cascade, inheritance, media queries, pseudo states, and browser behavior.&lt;/p&gt;
&lt;p&gt;Fluentic is not trying to make CSS disappear.&lt;/p&gt;
&lt;p&gt;It is trying to make component styling feel more explicit, composable, and predictable.&lt;/p&gt;
&lt;p&gt;The smallest version looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { style, type StyleProp } from '@fluentic/style';

const styles = {
  button: style({
    height: 36,
    paddingInline: 12,
    borderRadius: 6,
    border: '1px solid #d1d5db',
    backgroundColor: '#ffffff',
  }).hover({
    backgroundColor: '#f3f4f6',
  }),

  primary: style({
    borderColor: '#2563eb',
    backgroundColor: '#2563eb',
    color: '#ffffff',
  }),

  disabled: style({
    opacity: 0.5,
    pointerEvents: 'none',
  }),
};

type ToolbarButtonProps = React.ComponentProps&amp;lt;'button'&amp;gt; &amp;amp; {
  primary?: boolean;
  css?: StyleProp;
};

function ToolbarButton({
  primary,
  disabled,
  css,
  ...props
}: ToolbarButtonProps) {
  return (
    &amp;lt;button
      {...props}
      disabled={disabled}
      css={[
        styles.button,
        primary &amp;amp;&amp;amp; styles.primary,
        disabled &amp;amp;&amp;amp; styles.disabled,
        css,
      ]}
    /&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For reusable components, Fluentic can go further with tokens, slots, scopes, and &lt;code&gt;combineStyle(...)&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import {
  style,
  createTokens,
  combineStyle,
  bindScope,
  type StyleProp,
  type StyleTheme,
} from '@fluentic/style';

const button = createTokens({
  color: {
    surface: '#2563eb',
    surfaceHover: '#1d4ed8',
    text: '#ffffff',
    dangerSurface: '#dc2626',
  },
  space: {
    gap: 8,
    paddingInline: 12,
  },
});

const buttonStyles = {
  root: style.slot({
    display: 'inline-flex',
    alignItems: 'center',
    gap: button.space.gap,
    border: 0,
    borderRadius: 8,
    paddingBlock: 8,
    paddingInline: button.space.paddingInline,
    backgroundColor: button.color.surface,
    color: button.color.text,
  }).hover({
    backgroundColor: button.color.surfaceHover,
  }),

  icon: style.slot({
    display: 'inline-flex',
    width: 16,
    height: 16,
  }),

  label: style.slot({
    fontWeight: 650,
    lineHeight: 1,
  }),
};

type ButtonProps = {
  children: React.ReactNode;
  css?: StyleProp;
  icon?: React.ReactNode;
  theme?: StyleTheme;
};

function BaseButton(props: ButtonProps) {
  const css = combineStyle(
    buttonStyles,
    bindScope(buttonStyles.root, props.theme),
  );

  return (
    &amp;lt;button css={[css.root, props.css]}&amp;gt;
      {props.icon ? &amp;lt;span css={css.icon}&amp;gt;{props.icon}&amp;lt;/span&amp;gt; : null}
      &amp;lt;span css={css.label}&amp;gt;{props.children}&amp;lt;/span&amp;gt;
    &amp;lt;/button&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then variants can target public slots and component tokens instead of reaching through DOM structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const dangerButton = style.scope([
  button.color.surface(button.color.dangerSurface),

  buttonStyles.label({
    fontWeight: 700,
  }),
]);

const compactButton = style.scope([
  buttonStyles.root({
    gap: 6,
    paddingBlock: 6,
    paddingInline: 10,
  }),

  buttonStyles.label({
    fontSize: 13,
  }),
]);

function Button({
  danger,
  compact,
  theme,
  ...props
}: ButtonProps &amp;amp; {
  danger?: boolean;
  compact?: boolean;
}) {
  return (
    &amp;lt;BaseButton
      {...props}
      theme={[
        danger &amp;amp;&amp;amp; dangerButton,
        compact &amp;amp;&amp;amp; compactButton,
        theme,
      ]}
    /&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the app level, values can stay readable while still being themeable.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { createTheme, createValues, style } from '@fluentic/style';

const color = createValues([
  '#2563eb | Accent',
  '#1d4ed8 | AccentHover',
  '#ffffff | AccentText',
]);

const darkTheme = createTheme([
  color('#2563eb | Accent', '#60a5fa'),
  color('#1d4ed8 | AccentHover', '#93c5fd'),
  color('#ffffff | AccentText', '#0f172a'),
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the app can map those values into the component’s exposed tokens through a component theme:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const appButtonTheme = style.scope([
  button.color.surface(color('#2563eb | Accent')),
  button.color.surfaceHover(color('#1d4ed8 | AccentHover')),
  button.color.text(color('#ffffff | AccentText')),
]);

function App() {
  return (
    &amp;lt;main css={darkTheme}&amp;gt;
      &amp;lt;Button theme={appButtonTheme}&amp;gt;
        Save
      &amp;lt;/Button&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The app theme decides what the app values become.&lt;/p&gt;
&lt;p&gt;The component theme decides how this button connects to those values.&lt;/p&gt;
&lt;p&gt;The component itself stays explicit about the parts and tokens it exposes.&lt;/p&gt;
&lt;p&gt;The important part is not only the exact syntax.&lt;/p&gt;
&lt;p&gt;The important part is the styling approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;styles are defined as values&lt;/li&gt;
&lt;li&gt;styles are composed explicitly&lt;/li&gt;
&lt;li&gt;conditional styles are visible&lt;/li&gt;
&lt;li&gt;component parts can be named with slots&lt;/li&gt;
&lt;li&gt;variants can target slots without depending on DOM structure&lt;/li&gt;
&lt;li&gt;app and component themes can be prepared early without becoming a separate styling system&lt;/li&gt;
&lt;li&gt;selectors are available, but they are not the whole styling model&lt;/li&gt;
&lt;li&gt;CSS is extracted for production builds&lt;/li&gt;
&lt;li&gt;debugging should lead back to the React code, not stop at a generated class name&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the balance I want.&lt;/p&gt;
&lt;p&gt;React Native simplicity.&lt;/p&gt;
&lt;p&gt;CSS power.&lt;/p&gt;
&lt;p&gt;Modern React output.&lt;/p&gt;
&lt;p&gt;A styling system where the everyday path stays simple, explicit when the component grows and prepared for the app-level concerns that usually arrive later.&lt;/p&gt;
&lt;h3 id=&quot;why-not-just-use-existing-tools-&quot;&gt;Why Not Just Use Existing Tools?&lt;/h3&gt;
&lt;p&gt;This is a fair question.&lt;/p&gt;
&lt;p&gt;There are many good styling tools now.&lt;/p&gt;
&lt;p&gt;Some are utility-first.&lt;br&gt;
Some are CSS-in-TypeScript.&lt;br&gt;
Some are zero-runtime.&lt;br&gt;
Some are compiler-first.&lt;br&gt;
Some are design-system focused.&lt;br&gt;
Some are close to CSS Modules with better ergonomics.&lt;/p&gt;
&lt;p&gt;I am not building Fluentic because I think all of them are bad.&lt;/p&gt;
&lt;p&gt;I am building it because I want a very specific feel.&lt;/p&gt;
&lt;p&gt;The feel of React Native style composition, adapted to React web.&lt;/p&gt;
&lt;p&gt;The thing I miss is not only extraction, type safety, or colocated styles.&lt;/p&gt;
&lt;p&gt;It is the simple confidence of this line:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;style={[base, variant, state, props.style]}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That mental model has been living rent-free in my head for years.&lt;/p&gt;
&lt;p&gt;When I came back to React web, I wanted that feeling again.&lt;/p&gt;
&lt;p&gt;But I also wanted hover, focus, media queries, selectors, SSR, extraction, component slots, and CSS output.&lt;/p&gt;
&lt;p&gt;So Fluentic is my attempt to connect those worlds.&lt;/p&gt;
&lt;h3 id=&quot;what-i-am-trying-to-avoid&quot;&gt;What I Am Trying To Avoid&lt;/h3&gt;
&lt;p&gt;Fluentic is also shaped by the things I want to avoid from my own past web code.&lt;/p&gt;
&lt;p&gt;I want to avoid class names becoming a private styling API between parent and child components.&lt;/p&gt;
&lt;p&gt;I want to avoid parent styles reaching too deeply into child structure.&lt;/p&gt;
&lt;p&gt;I want to avoid selectors that make markup hard to change.&lt;/p&gt;
&lt;p&gt;I want to avoid styling systems where I cannot easily answer “which style wins?”&lt;/p&gt;
&lt;p&gt;I want to avoid too much runtime styling work.&lt;/p&gt;
&lt;p&gt;I want to avoid cleverness that feels good today and suspicious six months later.&lt;/p&gt;
&lt;p&gt;That last one is important.&lt;/p&gt;
&lt;p&gt;A lot of styling decisions feel great when writing them.&lt;/p&gt;
&lt;p&gt;The real test is how they feel when changing them later.&lt;/p&gt;
&lt;h3 id=&quot;fluentic-is-still-early&quot;&gt;Fluentic Is Still Early&lt;/h3&gt;
&lt;p&gt;Fluentic is still early.&lt;/p&gt;
&lt;p&gt;I am still exploring the API, constraints, compiler behavior, and the right balance between CSS power and React Native-like explicitness.&lt;/p&gt;
&lt;p&gt;But the direction is clear:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;React styling should feel more like composing values, and less like maintaining invisible relationships between class names and DOM structure.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is the idea I want to test.&lt;/p&gt;
&lt;p&gt;Not whether CSS can do everything. It can.&lt;/p&gt;
&lt;p&gt;Not whether extraction is useful. It is.&lt;/p&gt;
&lt;p&gt;The question is:&lt;/p&gt;
&lt;p&gt;Can styling for component-heavy React apps feel predictable again?&lt;/p&gt;
&lt;p&gt;Can we keep the parts of CSS that make the web powerful, while making everyday component styling feel closer to the React Native model?&lt;/p&gt;
&lt;p&gt;That is what I am building with Fluentic.&lt;/p&gt;
&lt;p&gt;You can check out Fluentic here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fluentic-dev/style&quot;&gt;https://github.com/fluentic-dev/style&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://fluenticstack.com/style&quot;&gt;https://fluenticstack.com/style&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If this problem feels familiar, especially if you have worked on both React web and React Native apps, I’d love to hear what you think.&lt;/p&gt;
&lt;p&gt;Which parts of React styling become harder to change over time?&lt;/p&gt;
&lt;p&gt;Which parts of React Native styling do you wish existed on React web?&lt;/p&gt;
&lt;p&gt;And what component-styling cases should Fluentic cover next?&lt;/p&gt;
&lt;p&gt;That feedback would be much more useful than stars alone, though I will not pretend stars hurt.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25725/building-fluentic-style-bringing-react-native-style-composition-to-react</guid>
<pubDate>Sat, 29 Aug 2026 01:22:22 +0000</pubDate>
</item>
<item>
<title>Beyond CSS-in-JS: From Element Styles To Component Themes</title>
<link>https://coderlegion.com/25403/beyond-css-in-js-from-element-styles-to-component-themes</link>
<description>&lt;p&gt;I started &lt;a rel=&quot;nofollow&quot; href=&quot;https://fluenticstack.com/style&quot;&gt;Fluentic Style&lt;/a&gt; with a small idea:&lt;/p&gt;
&lt;p&gt;I wanted React Native-style composition in React web.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;View style={[base, active &amp;amp;&amp;amp; activeStyle, props.style]} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That kind of style composition always felt practical to me.&lt;/p&gt;
&lt;p&gt;You have a base style.&lt;br&gt;
You add another style when some state is active.&lt;br&gt;
You still let styles come from props.&lt;/p&gt;
&lt;p&gt;So the first Fluentic idea looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;div css={[base, active &amp;amp;&amp;amp; selected, props.css]} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That was the starting point.&lt;/p&gt;
&lt;p&gt;I wrote about it here:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/omnidev/bringing-react-native-style-composition-to-react-d1l&quot;&gt;Bringing React Native Style Composition to React&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But the more Fluentic evolved, the clearer it became that the real problem was never just CSS-in-JS or build-time extraction.&lt;/p&gt;
&lt;p&gt;What felt missing was a better pattern for component-level styling, and for theming that can move from app-wide defaults down to a specific component.&lt;/p&gt;
&lt;p&gt;The bigger question became:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Can a styling system cover the everyday path from styling one element, to styling reusable components, to component themes, to app-wide themes, while still keeping debugging and production extraction usable?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is the version of Fluentic I am building now.&lt;/p&gt;
&lt;p&gt;Not just another CSS-in-JS syntax.&lt;/p&gt;
&lt;p&gt;More like an attempt to connect the parts of styling that often feel split apart.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-is-not-only-putting-css-in-js&quot;&gt;The Problem Is Not Only Putting CSS In JS&lt;/h3&gt;
&lt;p&gt;CSS-in-JS gave frontend developers something useful.&lt;/p&gt;
&lt;p&gt;It made styles feel closer to components.&lt;br&gt;
It made dynamic styling easier.&lt;br&gt;
It made local styling easier.&lt;br&gt;
It made React UI feel less split between a component file and a separate stylesheet.&lt;/p&gt;
&lt;p&gt;But daily app styling is bigger than:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;How do I put CSS on this DOM element?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is one layer.&lt;/p&gt;
&lt;p&gt;A real app usually moves through several layers:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;element style&lt;br&gt;
component style&lt;br&gt;
component variant&lt;br&gt;
component theme&lt;br&gt;
app-wide theme&lt;br&gt;
production output&lt;br&gt;
debugging&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Many styling approaches are good at one or two of those.&lt;/p&gt;
&lt;p&gt;Some are great for quick element styles.&lt;br&gt;
Some are great for app-wide tokens.&lt;br&gt;
Some are great for utility classes.&lt;br&gt;
Some are great for local CSS files.&lt;br&gt;
Some are great for runtime dynamic values.&lt;br&gt;
Some are great for extracted CSS.&lt;/p&gt;
&lt;p&gt;But in real app work, I kept wanting those layers to connect better.&lt;/p&gt;
&lt;p&gt;That became the bigger Fluentic goal:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;style composition from elements, to components, to component themes, to app-wide themes&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;without turning each layer into a separate styling system.&lt;/p&gt;
&lt;h3 id=&quot;start-with-one-element&quot;&gt;Start With One Element&lt;/h3&gt;
&lt;p&gt;The smallest case should stay small.&lt;/p&gt;
&lt;p&gt;If I only need to style one element, I should be able to write one style and attach it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { style } from '@fluentic/style';

const saveButton = style({
  borderRadius: 8,
  padding: '8px 12px',
});

&amp;lt;button css={saveButton}&amp;gt;Save changes&amp;lt;/button&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If that element needs hover, media, or state styles, it should still stay attached to the same style idea:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const saveButton = style({
  borderRadius: 8,
  padding: '8px 12px',
}).hover({
  backgroundColor: '#f3f4f6',
});

&amp;lt;button css={saveButton}&amp;gt;Save changes&amp;lt;/button&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For teams that like utility-class authoring, Fluentic can also work closer to that style:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const saveButton = cx(
  'inline-flex',
  'items-center',
  'rounded-md',
  'px-3',
  'py-2',
).hover('bg-slate-100');

&amp;lt;button css={saveButton}&amp;gt;Save changes&amp;lt;/button&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So Fluentic does not start by forcing every use case into components, themes, or design-system structure.&lt;/p&gt;
&lt;p&gt;Sometimes you just need to style an element.&lt;/p&gt;
&lt;p&gt;That should be easy.&lt;/p&gt;
&lt;h3 id=&quot;then-the-element-becomes-a-component&quot;&gt;Then The Element Becomes A Component&lt;/h3&gt;
&lt;p&gt;The next problem appears when that styled element becomes reusable.&lt;/p&gt;
&lt;p&gt;A button is not always just one DOM node.&lt;/p&gt;
&lt;p&gt;It can have parts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function Button(props) {
  return (
    &amp;lt;button&amp;gt;
      &amp;lt;span&amp;gt;{props.icon}&amp;lt;/span&amp;gt;
      &amp;lt;span&amp;gt;{props.children}&amp;lt;/span&amp;gt;
    &amp;lt;/button&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the styling question changes.&lt;/p&gt;
&lt;p&gt;There is a root.&lt;br&gt;
There is an icon.&lt;br&gt;
There is a label.&lt;/p&gt;
&lt;p&gt;Maybe a loading indicator.&lt;br&gt;
Maybe an action area.&lt;br&gt;
Maybe a wrapper around some third-party element.&lt;/p&gt;
&lt;p&gt;Frontend teams already think in these names:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;root, icon, label, content, trigger, item, control, thumb, track, portal&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The old HTML/CSS way would usually name those parts with classes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.button { ... }
.button__icon { ... }
.button__label { ... }
.button[data-danger] .button__label { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is understandable.&lt;/p&gt;
&lt;p&gt;But in a component system, the component owns the inside.&lt;/p&gt;
&lt;p&gt;So when the outside needs to style those parts, React APIs often grow shapes like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;Button
  className=&quot;danger-button&quot;
  iconClassName=&quot;danger-icon&quot;
  labelClassName=&quot;danger-label&quot;
&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;Button
  classes={{
    root: 'danger-button',
    icon: 'danger-icon',
    label: 'danger-label',
  }}
&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;Button
  styles={{
    label: { fontWeight: 700 },
  }}
&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are practical. I have written APIs like this many times.&lt;/p&gt;
&lt;p&gt;But the repeated shape is clear:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;the component has parts, and outside code needs a supported way to style those parts&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Fluentic’s answer is &lt;code&gt;style.slot(...)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A slot is a normal style for the component itself, but it also gives that component part an identity that outside styles can target later.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const buttonStyles = {
  root: style.slot({
    borderRadius: 8,
    padding: '8px 12px',
  }),

  icon: style({
    flex: '0 0 auto',
  }),

  label: style.slot({
    fontWeight: 700,
  }),
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that not every internal element needs to be a slot.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;icon&lt;/code&gt; can stay private with &lt;code&gt;style(...)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;root&lt;/code&gt; and &lt;code&gt;label&lt;/code&gt; become public styling targets with &lt;code&gt;style.slot(...)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That is the first important split:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;use &lt;code&gt;style(...)&lt;/code&gt; for ordinary element styles&lt;br&gt;
use &lt;code&gt;style.slot(...)&lt;/code&gt; when a component part should be styleable from outside&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;component-themes-are-not-the-same-as-app-themes&quot;&gt;Component Themes Are Not The Same As App Themes&lt;/h3&gt;
&lt;p&gt;Once a component exposes slots, outside code needs a way to provide styles for those slots.&lt;/p&gt;
&lt;p&gt;That is where &lt;code&gt;style.scope(...)&lt;/code&gt; comes in.&lt;/p&gt;
&lt;p&gt;A scope groups changes for public component parts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const dangerButton = style.scope([
  buttonStyles.root({
    backgroundColor: '#dc2626',
  }),

  buttonStyles.label({
    color: 'white',
  }),
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The component receives that as a theme-like input:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;type ButtonProps = {
  children: React.ReactNode;
  icon?: React.ReactNode;
  theme?: StyleTheme;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the component prepares the styles it will render:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function Button(props: ButtonProps) {
  const css = combineStyle(
    buttonStyles,
    bindScope(buttonStyles.root, props.theme),
  );

  return (
    &amp;lt;button css={css.root}&amp;gt;
      {props.icon ? &amp;lt;span css={css.icon}&amp;gt;{props.icon}&amp;lt;/span&amp;gt; : null}
      &amp;lt;span css={css.label}&amp;gt;{props.children}&amp;lt;/span&amp;gt;
    &amp;lt;/button&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is different from passing a raw class name into each part.&lt;/p&gt;
&lt;p&gt;The outside says what it wants to change:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;Button theme={dangerButton}&amp;gt;Delete&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The component still decides where those styles attach.&lt;/p&gt;
&lt;p&gt;That is why I think of scopes as component-facing themes.&lt;/p&gt;
&lt;p&gt;They are not only global colors.&lt;/p&gt;
&lt;p&gt;They can change slots, tokens, states, media rules, and multiple component parts together.&lt;/p&gt;
&lt;h3 id=&quot;app-wide-themes-need-a-different-layer&quot;&gt;App-Wide Themes Need A Different Layer&lt;/h3&gt;
&lt;p&gt;App-wide theming is related, but it is not exactly the same problem.&lt;/p&gt;
&lt;p&gt;Sometimes the app wants to define shared values:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const color = createTokens({
  surface: '#ffffff',
  text: '#111827',
  accent: '#2563eb',
  danger: '#dc2626',
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A style can use those tokens:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const page = style({
  backgroundColor: color.surface,
  color: color.text,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then an app theme can override those token values:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const darkTheme = createTheme([
  color.surface('#0f172a'),
  color.text('#f8fafc'),
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is app-facing theming.&lt;/p&gt;
&lt;p&gt;It answers:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;What should &lt;code&gt;surface&lt;/code&gt;, &lt;code&gt;text&lt;/code&gt;, or &lt;code&gt;accent&lt;/code&gt; mean in this part of the app?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A component theme answers a different question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;How should this component’s public parts or component-specific tokens change?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Fluentic supports both.&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;createTheme(...)&lt;/code&gt; when the theme only changes token values.&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;style.scope(...)&lt;/code&gt; when the theme needs to affect component slots, component tokens, states, media conditions, or multiple parts together.&lt;/p&gt;
&lt;p&gt;This distinction matters because app-wide themes and component themes often live in different realms.&lt;/p&gt;
&lt;p&gt;A design system may define app tokens.&lt;/p&gt;
&lt;p&gt;A component library may expose component tokens and slots.&lt;/p&gt;
&lt;p&gt;An app may need to map app values into component values.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const button = createTokens({
  color: {
    surface: '#2563eb',
    surfaceHover: '#1d4ed8',
    text: '#ffffff',
  },
});

const appButtonTheme = style.scope([
  button.color.surface(color.accent),
  button.color.surfaceHover(color.accent),
  button.color.text('#ffffff'),
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The app theme controls what &lt;code&gt;color.accent&lt;/code&gt; means.&lt;/p&gt;
&lt;p&gt;The component theme says how the button should use that app value.&lt;/p&gt;
&lt;p&gt;That is the kind of bridge I wanted Fluentic to support.&lt;/p&gt;
&lt;p&gt;Not app theming over here, component theming over there, and manual glue in between.&lt;/p&gt;
&lt;p&gt;One styling path that can connect them.&lt;/p&gt;
&lt;h3 id=&quot;variants-become-scopes-too&quot;&gt;Variants Become Scopes Too&lt;/h3&gt;
&lt;p&gt;Component variants are usually where styling logic starts getting busy.&lt;/p&gt;
&lt;p&gt;A variant like this looks simple:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;Button size=&quot;sm&quot; tone=&quot;danger&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But internally it may affect several parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;root padding&lt;/li&gt;
&lt;li&gt;label font size&lt;/li&gt;
&lt;li&gt;icon size&lt;/li&gt;
&lt;li&gt;background color&lt;/li&gt;
&lt;li&gt;hover color&lt;/li&gt;
&lt;li&gt;disabled color&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without a shared styling shape, that often becomes a matrix of class names or conditionals.&lt;/p&gt;
&lt;p&gt;In Fluentic, a variant can be a scope:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const compactButton = style.scope([
  buttonStyles.root({
    gap: 6,
    paddingBlock: 6,
    paddingInline: 10,
  }),

  buttonStyles.label({
    fontSize: 13,
  }),
]);

const dangerButton = style.scope([
  buttonStyles.root({
    backgroundColor: '#dc2626',
  }),

  buttonStyles.label({
    color: 'white',
  }),
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And because scopes are values, they compose:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;Button
  theme={[
    compact &amp;amp;&amp;amp; compactButton,
    danger &amp;amp;&amp;amp; dangerButton,
    props.theme,
  ]}
&amp;gt;
  Delete
&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is where Fluentic starts feeling bigger than element-level CSS-in-JS.&lt;/p&gt;
&lt;p&gt;The same composition idea can move from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;div css={[base, active &amp;amp;&amp;amp; selected]} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;Button theme={[compact &amp;amp;&amp;amp; compactButton, danger &amp;amp;&amp;amp; dangerButton]} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The shape stays familiar.&lt;/p&gt;
&lt;p&gt;The layer changes.&lt;/p&gt;
&lt;h3 id=&quot;debugging-still-has-to-follow-the-whole-path&quot;&gt;Debugging Still Has To Follow The Whole Path&lt;/h3&gt;
&lt;p&gt;A nice authoring API is not enough if generated CSS becomes a dead end.&lt;/p&gt;
&lt;p&gt;That is one of the places where I think styling tools often ask developers to accept too much friction.&lt;/p&gt;
&lt;p&gt;You write nice code.&lt;/p&gt;
&lt;p&gt;DevTools shows generated class names or injected CSS.&lt;/p&gt;
&lt;p&gt;Then you have to guess where the rule came from.&lt;/p&gt;
&lt;p&gt;Fluentic emits atomic CSS, but not only for output size or dedupe.&lt;/p&gt;
&lt;p&gt;Atomic CSS also gives a useful debugging unit:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;one generated rule can map back to one styling decision&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;style({
  backgroundColor: '#2563eb',
}).hover({
  backgroundColor: '#1d4ed8',
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A generated hover &lt;code&gt;background-color&lt;/code&gt; rule should be able to point back to the &lt;code&gt;backgroundColor&lt;/code&gt; inside &lt;code&gt;.hover(...)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For slots and scopes, the trace should stay meaningful too.&lt;/p&gt;
&lt;p&gt;If this wins:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const dangerButton = style.scope([
  buttonStyles.label({
    color: 'white',
  }),
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the generated &lt;code&gt;color&lt;/code&gt; rule should be able to lead back to the scoped &lt;code&gt;label&lt;/code&gt; override.&lt;/p&gt;
&lt;p&gt;That is why Fluentic keeps debug metadata and sourcemap information in development.&lt;/p&gt;
&lt;p&gt;The debug story should follow the same path:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;style → slot → scope → generated CSS → source trace&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If component styling is part of the model, debugging component styling has to be part of the model too.&lt;/p&gt;
&lt;h3 id=&quot;production-extraction-should-fit-the-same-model&quot;&gt;Production Extraction Should Fit The Same Model&lt;/h3&gt;
&lt;p&gt;The other big requirement is production.&lt;/p&gt;
&lt;p&gt;A runtime-only CSS-in-JS library can be convenient, especially in small apps.&lt;/p&gt;
&lt;p&gt;But production apps need more.&lt;/p&gt;
&lt;p&gt;They need extracted CSS.&lt;br&gt;
They need SSR and framework integrations.&lt;br&gt;
They need predictable output.&lt;br&gt;
They need the runtime to do less work when the build can prepare things ahead of time.&lt;/p&gt;
&lt;p&gt;Fluentic’s compiler and bundler plugins extract the static style declarations from patterns like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;style(...)
style.slot(...)
style.scope(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and emit extracted atomic CSS plus prepared JavaScript output.&lt;/p&gt;
&lt;p&gt;But real apps still have runtime-known choices:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;Button
  theme={[
    compact &amp;amp;&amp;amp; compactButton,
    danger &amp;amp;&amp;amp; dangerButton,
    props.theme,
  ]}
&amp;gt;
  Delete
&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The build can prepare CSS from the style declarations.&lt;/p&gt;
&lt;p&gt;The runtime still resolves which themes, scopes, token values, and prop-driven choices apply during render.&lt;/p&gt;
&lt;p&gt;That is important.&lt;/p&gt;
&lt;p&gt;I do not want a system where extracted CSS only works for the boring cases, and the moment a component becomes dynamic you move to a different styling model.&lt;/p&gt;
&lt;p&gt;The goal is one authoring model:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;build extracts what can be prepared&lt;br&gt;
runtime resolves what the app decides while rendering&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;syntax-should-be-flexible-but-the-path-should-stay-connected&quot;&gt;Syntax Should Be Flexible, But The Path Should Stay Connected&lt;/h3&gt;
&lt;p&gt;CSS-in-JS discussions often become syntax discussions.&lt;/p&gt;
&lt;p&gt;Objects.&lt;br&gt;
Tagged templates.&lt;br&gt;
Utility classes.&lt;br&gt;
Inline styles.&lt;br&gt;
Generated classes.&lt;br&gt;
CSS files.&lt;/p&gt;
&lt;p&gt;I understand why. Syntax is what developers touch every day.&lt;/p&gt;
&lt;p&gt;Fluentic started object-first:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const card = style({
  padding: 16,
  borderRadius: 12,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But Fluentic now also supports Tailwind-like class-name authoring:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const card = cx('rounded-xl', 'bg-white', 'p-4');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It also supports Tailwind-style object presets and custom transforms.&lt;/p&gt;
&lt;p&gt;A design system can create its own vocabulary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const panel = ui({
  row: true,
  center: true,
  gapX: 12,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The point is not that every team should write styles the same way.&lt;/p&gt;
&lt;p&gt;The point is that different syntaxes can still feed the same Fluentic path.&lt;/p&gt;
&lt;p&gt;Once they become Fluentic styles, they can still participate in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;element styling&lt;/li&gt;
&lt;li&gt;slots&lt;/li&gt;
&lt;li&gt;scopes&lt;/li&gt;
&lt;li&gt;token themes&lt;/li&gt;
&lt;li&gt;component themes&lt;/li&gt;
&lt;li&gt;atomic CSS output&lt;/li&gt;
&lt;li&gt;production extraction&lt;/li&gt;
&lt;li&gt;sourcemaps&lt;/li&gt;
&lt;li&gt;debug metadata&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the practical reason custom transforms matter.&lt;/p&gt;
&lt;p&gt;They are not only a syntax trick.&lt;/p&gt;
&lt;p&gt;They let a team adapt the authoring vocabulary without leaving the rest of the styling system behind.&lt;/p&gt;
&lt;h3 id=&quot;frameworks-need-bridges-not-a-new-styling-model&quot;&gt;Frameworks Need Bridges, Not A New Styling Model&lt;/h3&gt;
&lt;p&gt;Fluentic started with React.&lt;/p&gt;
&lt;p&gt;React is still the main focus.&lt;/p&gt;
&lt;p&gt;But modern frontend is not only one JSX/runtime story.&lt;/p&gt;
&lt;p&gt;Next.js App Router brings server components, server/client rendering, HMR, streaming concerns, development CSS, sourcemaps, and production extraction.&lt;/p&gt;
&lt;p&gt;SolidJS has its own compiler-centered JSX path.&lt;/p&gt;
&lt;p&gt;Preact has a different prop shape.&lt;/p&gt;
&lt;p&gt;So Fluentic is moving toward a stable styling core with framework-specific bridges.&lt;/p&gt;
&lt;p&gt;React can use Fluentic’s JSX runtime, or it can use a &lt;code&gt;cssProp&lt;/code&gt; compiler transform.&lt;/p&gt;
&lt;p&gt;Next.js gets a deeper integration for App Router, RSC development styling, debug CSS, sourcemaps, and production extraction.&lt;/p&gt;
&lt;p&gt;SolidJS client-side support uses a &lt;code&gt;css&lt;/code&gt; prop transform before Solid’s compiler.&lt;/p&gt;
&lt;p&gt;The goal is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;keep the style path stable, adapt the framework handoff&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That keeps Fluentic from being only a React runtime library.&lt;/p&gt;
&lt;p&gt;The style ideas can stay the same while the JSX integration changes per framework.&lt;/p&gt;
&lt;h3 id=&quot;what-fluentic-currently-has&quot;&gt;What Fluentic Currently Has&lt;/h3&gt;
&lt;p&gt;Fluentic Style is still new and currently in beta.&lt;/p&gt;
&lt;p&gt;But it already covers much more than my first small idea.&lt;/p&gt;
&lt;p&gt;Authoring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;style(...)&lt;/code&gt; object styles&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cx(...)&lt;/code&gt; class-name authoring&lt;/li&gt;
&lt;li&gt;Tailwind-like class-name preset&lt;/li&gt;
&lt;li&gt;Tailwind-style object preset&lt;/li&gt;
&lt;li&gt;custom style functions&lt;/li&gt;
&lt;li&gt;custom transforms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Element and component composition:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;css&lt;/code&gt; prop arrays&lt;/li&gt;
&lt;li&gt;conditional styles&lt;/li&gt;
&lt;li&gt;&lt;code&gt;style.slot(...)&lt;/code&gt; for public component parts&lt;/li&gt;
&lt;li&gt;&lt;code&gt;style.scope(...)&lt;/code&gt; for component themes and slot overrides&lt;/li&gt;
&lt;li&gt;&lt;code&gt;combineStyle(...)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bindScope(...)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Theming:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;createToken(...)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;createTokens(...)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;createTheme(...)&lt;/code&gt; for token-only themes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;style.scope(...)&lt;/code&gt; for component themes, slot overrides, and token mapping&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Build and debug:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;atomic CSS in development and production&lt;/li&gt;
&lt;li&gt;production CSS extraction&lt;/li&gt;
&lt;li&gt;prepared JavaScript output&lt;/li&gt;
&lt;li&gt;debug metadata&lt;/li&gt;
&lt;li&gt;sourcemap support&lt;/li&gt;
&lt;li&gt;development utilities for inspecting generated styles&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Frameworks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;Next.js App Router&lt;/li&gt;
&lt;li&gt;SolidJS client-side support&lt;/li&gt;
&lt;li&gt;Preact-oriented adapter path&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is far beyond the original idea.&lt;/p&gt;
&lt;p&gt;The first goal was:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;bring React Native-style composition to React web&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The current goal is closer to:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;make everyday styling composition work from element styles to component styles to component themes to app-wide themes, with debugging and production output still part of the same story&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is the part that keeps me building it.&lt;/p&gt;
&lt;h3 id=&quot;closing&quot;&gt;Closing&lt;/h3&gt;
&lt;p&gt;I do not think existing CSS-in-JS approaches are wrong.&lt;/p&gt;
&lt;p&gt;I do not think CSS is broken.&lt;/p&gt;
&lt;p&gt;A lot of styling approaches are useful, and I still use many familiar patterns.&lt;/p&gt;
&lt;p&gt;Fluentic is my attempt to connect the parts that I kept seeing split apart:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;quick element styling&lt;/li&gt;
&lt;li&gt;component styling&lt;/li&gt;
&lt;li&gt;component theming&lt;/li&gt;
&lt;li&gt;app-wide theming&lt;/li&gt;
&lt;li&gt;style composition&lt;/li&gt;
&lt;li&gt;generated CSS debugging&lt;/li&gt;
&lt;li&gt;production extraction&lt;/li&gt;
&lt;li&gt;framework integration&lt;/li&gt;
&lt;li&gt;flexible authoring syntax&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It is still beta, and I expect the API to keep improving.&lt;/p&gt;
&lt;p&gt;But the direction is clear now.&lt;/p&gt;
&lt;p&gt;Fluentic is no longer only my attempt to bring React Native-style arrays to React web.&lt;/p&gt;
&lt;p&gt;It is an attempt to make styling feel more complete for component-based apps: from one element, to a reusable component, to local component themes, to app-wide themes, while still caring about CSS output, DevTools, production builds, and framework realities.&lt;/p&gt;
&lt;p&gt;Useful links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://fluenticstack.com/style&quot;&gt;Fluentic Style Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://fluenticstack.com/style/docs/reference/style-slot/&quot;&gt;style.slot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://fluenticstack.com/style/docs/reference/style-scope/&quot;&gt;style.scope&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://fluenticstack.com/style/docs/reference/combine-style/&quot;&gt;combineStyle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://fluenticstack.com/style/docs/reference/create-tokens/&quot;&gt;createTokens&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://fluenticstack.com/style/docs/reference/create-theme/&quot;&gt;createTheme&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fluentic-dev/style&quot;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/@fluentic/style&quot;&gt;npm: @fluentic/style&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/omnidev/bringing-react-native-style-composition-to-react-d1l&quot;&gt;Original post: Bringing React Native Style Composition to React&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25403/beyond-css-in-js-from-element-styles-to-component-themes</guid>
<pubDate>Wed, 26 Aug 2026 04:30:40 +0000</pubDate>
</item>
<item>
<title>Common CSS Mistakes Beginners Make (and How to Fix Them)</title>
<link>https://coderlegion.com/23008/common-css-mistakes-beginners-make-and-how-to-fix-them</link>
<description>&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Every one of these mistakes cost me real debugging time when I was learning. Not because CSS is inherently confusing — but because nobody explained why these things happen, just that they do. Understanding the &quot;why&quot; is what actually prevents you from repeating them.&lt;/p&gt;
&lt;h2 id=&quot;1-the-box-model-confusion&quot;&gt;1. The Box Model Confusion&lt;/h2&gt;
&lt;p&gt;By default, &lt;strong&gt;width&lt;/strong&gt; and &lt;strong&gt;height&lt;/strong&gt; don't include padding and border — meaning a box you set to &lt;strong&gt;300px&lt;/strong&gt; wide can visually render wider once padding is added.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.box {
  width: 300px;
  padding: 20px;
  border: 2px solid black;
  /* Actual rendered width: 300 + 20+20 + 2+2 = 344px */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: set this once, globally, at the start of every project:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* {
  box-sizing: border-box;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;strong&gt;width: 300px&lt;/strong&gt; means the element is always 300px total, padding and border included. This single line prevents a huge number of layout bugs.&lt;/p&gt;
&lt;h2 id=&quot;2-margin-collapse&quot;&gt;2. Margin Collapse&lt;/h2&gt;
&lt;p&gt;Two vertical margins between sibling elements don't add together — they collapse into the larger of the two.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.first { margin-bottom: 20px; }
.second { margin-top: 30px; }
/* Actual gap between them: 30px, NOT 50px */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This confuses beginners constantly because it seems inconsistent — but it's expected browser behavior for vertical margins specifically (doesn't apply to horizontal margins, or to Flexbox/Grid &lt;code&gt;gap&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The fix: use &lt;code&gt;gap&lt;/code&gt; on a flex or grid container instead of relying on margins between children — it doesn't collapse and is far more predictable.&lt;/p&gt;
&lt;h2 id=&quot;3-overusing-important&quot;&gt;3. Overusing &lt;code&gt;!important&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;.button {
  background: blue !important;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;!important&lt;/code&gt; overrides normal CSS specificity rules — which feels like a quick fix, but it creates a mess later. Once you have several &lt;code&gt;!important&lt;/code&gt; rules fighting each other, debugging becomes genuinely painful because normal specificity logic no longer applies.&lt;/p&gt;
&lt;p&gt;The fix: understand specificity instead. IDs beat classes, classes beat elements. If you're reaching for &lt;code&gt;!important&lt;/code&gt;, it usually means a more specific selector (or restructuring your CSS) would solve it more cleanly.&lt;/p&gt;
&lt;h2 id=&quot;4-not-understanding-position-absolute&quot;&gt;4. Not Understanding &lt;code&gt;position: absolute&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;.tooltip {
  position: absolute;
  top: 0;
  left: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Beginners often expect this to position relative to the whole page. In reality, &lt;code&gt;position: absolute&lt;/code&gt; positions relative to the nearest ancestor with &lt;code&gt;position: relative&lt;/code&gt; (or &lt;code&gt;absolute/fixed&lt;/code&gt;) set. If no ancestor has that, it &lt;code&gt;positions relative&lt;/code&gt; to the whole document — which is rarely what you actually want.&lt;/p&gt;
&lt;p&gt;The fix: always set &lt;code&gt;position: relative&lt;/code&gt; on the parent container first:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.card {
  position: relative;
}
.badge {
  position: absolute;
  top: 8px;
  right: 8px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;5-fighting-flexbox-grid-instead-of-reading-the-docs&quot;&gt;5. Fighting Flexbox/Grid Instead of Reading the Docs&lt;/h2&gt;
&lt;p&gt;A very common beginner pattern: not getting the exact spacing/alignment you want, then adding random margins and padding until it &quot;looks right&quot; by trial and error.&lt;/p&gt;
&lt;p&gt;The fix: learn &lt;code&gt;justify-content, align-items,&lt;/code&gt; and &lt;code&gt;gap&lt;/code&gt; properly (covered fully in Article 2). Once you understand these three properties, most alignment problems have a direct, one-line solution instead of a pile of guessed margins.&lt;/p&gt;
&lt;h2 id=&quot;6-not-resetting-default-browser-styles&quot;&gt;6. Not Resetting Default Browser Styles&lt;/h2&gt;
&lt;p&gt;Browsers apply their own default styles to elements (&lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt; gets bullet points and padding, &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; gets specific margins, etc.), and these defaults are inconsistent across browsers.&lt;/p&gt;
&lt;p&gt;The fix: use a simple CSS reset at the top of your stylesheet, or use Tailwind CSS, which includes a solid reset (Preflight) automatically.&lt;/p&gt;
&lt;h2 id=&quot;7-specificity-wars-from-nested-selectors&quot;&gt;7. Specificity Wars from Nested Selectors&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;.card .header .title span {
  color: red;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Deeply nested selectors like this become extremely hard to override later, since they carry heavy specificity.&lt;/p&gt;
&lt;p&gt;The fix: keep selectors as flat as possible. A single well-named class (&lt;code&gt;.card-title&lt;/code&gt;) is easier to maintain than four levels of nesting.&lt;/p&gt;
&lt;h2 id=&quot;final-thoughts-1&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Almost every CSS mistake on this list comes down to one thing: not understanding why the browser is doing what it's doing, and reaching for a quick patch instead. Once you understand the box model, specificity, and positioning contexts properly, CSS debugging goes from guesswork to genuinely logical problem-solving.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Muhammad Farhan&lt;/strong&gt; is a Frontend Developer specializing in React.js and Tailwind CSS, based in Dera Ismail Khan, Pakistan.&lt;br&gt;
&lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;strong&gt;&lt;em&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://muhammadfarhandev.netlify.app&quot;&gt;Muhammad Farhan&lt;/a&gt;&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23008/common-css-mistakes-beginners-make-and-how-to-fix-them</guid>
<pubDate>Tue, 25 Aug 2026 05:00:00 +0000</pubDate>
</item>
<item>
<title>Understanding exec() in FSCSS — Two Completely Different Jobs</title>
<link>https://coderlegion.com/25158/understanding-exec-in-fscss-two-completely-different-jobs</link>
<description>&lt;h3 id=&quot;understanding-exec-in-fscss&quot;&gt;Understanding &lt;code&gt;exec()&lt;/code&gt; in FSCSS&lt;/h3&gt;
&lt;p&gt;In FSCSS, &lt;strong&gt;&lt;code&gt;exec()&lt;/code&gt;&lt;/strong&gt; serves two completely different purposes depending on where you write it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Inside your stylesheet: as a utility / debugging function  &lt;/li&gt;
&lt;li&gt;In JavaScript / HTML: to load and compile the styles&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let’s break both down clearly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-inside-stylesheets-debugging-amp-logging&quot;&gt;1. Inside Stylesheets: Debugging &amp;amp; Logging&lt;/h3&gt;
&lt;p&gt;When used inside an &lt;code&gt;@event&lt;/code&gt; block or after processing an array, &lt;code&gt;exec()&lt;/code&gt; lets you run small JavaScript helpers directly from your CSS.&lt;br&gt;
Its main job here is &lt;strong&gt;printing debug information&lt;/strong&gt; to the browser console so you can check if your logic is working.&lt;/p&gt;
&lt;h4 id=&quot;console-logging&quot;&gt;Console Logging&lt;/h4&gt;
&lt;p&gt;You can pass flags like &lt;code&gt;_log&lt;/code&gt;, &lt;code&gt;_warn&lt;/code&gt;, &lt;code&gt;_info&lt;/code&gt;, or &lt;code&gt;_error&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@event scaleSize(e) {
  if e &amp;gt;= 8 {
    return| num(8*2)em exec(_log, &quot;Scaling to 16em for optimal readability&quot;)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;checking-array-data&quot;&gt;Checking Array Data&lt;/h4&gt;
&lt;p&gt;You can also inspect the current values inside an FSCSS array:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@arr fruits[ apple, orange, banana ]

/* Prints: apple orange banana straight to your browser console */
exec(_log, &quot;@arr.fruits&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is extremely useful while building complex logic with &lt;code&gt;@event&lt;/code&gt;, &lt;code&gt;@arr&lt;/code&gt;, or calculated values.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-in-javascript-amp-imports-compilation-engine&quot;&gt;2. In JavaScript &amp;amp; Imports: Compilation Engine&lt;/h3&gt;
&lt;p&gt;Outside the stylesheet, &lt;code&gt;exec&lt;/code&gt; is the &lt;strong&gt;core engine&lt;/strong&gt; that compiles and runs FSCSS source code.&lt;/p&gt;
&lt;h4 id=&quot;cdn-initialization-html-&quot;&gt;CDN Initialization (HTML)&lt;/h4&gt;
&lt;p&gt;The compiler file loaded via CDN is named &lt;code&gt;exec.min.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/fscss@1.1.25/exec.min.js&quot; defer&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;style&amp;gt;
  /* Tells the engine to execute the initialization for a module */
  @import(exec(_init circle-progress))
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;programmatic-javascript-usage&quot;&gt;Programmatic JavaScript Usage&lt;/h4&gt;
&lt;p&gt;You can also import and control the compiler directly from JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { exec } from &quot;https://cdn.jsdelivr.net/npm/fscss@1.1.25/e/xfscss.min.js&quot;;

new exec({
  type: &quot;URL&quot;,
  content: &quot;styles.fscss&quot;,
  onSuccess: &quot;Styles loaded successfully!&quot;,
  onError: &quot;Oops, style compilation failed!&quot;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives you full control over loading, success messages, and error handling.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;quick-summary&quot;&gt;Quick Summary&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Location              &lt;/th&gt;&lt;th&gt;Purpose                              &lt;/th&gt;&lt;th&gt;Common Use Case&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Inside &lt;code&gt;.fscss&lt;/code&gt;       &lt;/td&gt;&lt;td&gt;Debugging &amp;amp; console logging          &lt;/td&gt;&lt;td&gt;Check &lt;code&gt;@event&lt;/code&gt; logic or array values&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;In HTML / JavaScript  &lt;/td&gt;&lt;td&gt;Compile and run FSCSS                &lt;/td&gt;&lt;td&gt;Load styles or initialize modules&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two different jobs. Same name.&lt;br&gt;
Once you know the context, &lt;code&gt;exec()&lt;/code&gt; becomes much clearer.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Useful links&lt;/strong&gt;  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docs → &lt;a rel=&quot;nofollow&quot; href=&quot;https://fscss.devtem.org&quot;&gt;fscss.devtem.org&lt;/a&gt;  &lt;/li&gt;
&lt;li&gt;vscode extention &lt;a rel=&quot;nofollow&quot; href=&quot;https://marketplace.visualstudio.com/items?itemName=Figsh.fscss&quot;&gt;FSCSS Support&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;npm → &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/fscss&quot;&gt;npmjs.com/package/fscss&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/25158/understanding-exec-in-fscss-two-completely-different-jobs</guid>
<pubDate>Sat, 22 Aug 2026 22:45:11 +0000</pubDate>
</item>
<item>
<title>pattern() in FSCSS 1.1.25</title>
<link>https://coderlegion.com/24831/pattern-in-fscss-1-1-25</link>
<description>&lt;p&gt;FSCSS v1.1.25 ships a new core method: &lt;strong&gt;&lt;code&gt;pattern()&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;You describe what you want in plain language, and FSCSS matches the closest pattern at compile time.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;pattern(0.5: &quot;Hello World Card&quot;, `
  border-radius: 12px;
  padding: 24px;
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: #fff;
  box-shadow: 0 8px 24px rgba(0,0,0,0.25);
`)

.card {
  hello world card
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compiles to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.card {
  border-radius: 12px;
  padding: 24px;
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: #fff;
  box-shadow: 0 8px 24px rgba(0,0,0,0.25);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice &lt;code&gt;.card&lt;/code&gt; doesn't call anything — it just contains a phrase close enough to &lt;code&gt;&quot;Hello World card&quot;&lt;/code&gt; to trigger the match.&lt;/p&gt;
&lt;h3 id=&quot;why-this-exists&quot;&gt;Why this exists&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@define&lt;/code&gt; is precise but rigid. You call it by exact name, with exact arguments. That's great for a library you built yourself, but it breaks down in a few real situations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Design handoffs.&lt;/strong&gt; A designer or junior dev writes &lt;code&gt;dark card with rounded corners&lt;/code&gt; inside a rule instead of hunting through your define library for the right call signature.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI-generated markup.&lt;/strong&gt; Tools that generate FSCSS from natural-language prompts can emit descriptive phrases instead of needing to know your exact mixin names.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fast prototyping.&lt;/strong&gt; You don't always remember what you named something six months ago. &lt;code&gt;pattern()&lt;/code&gt; lets close-enough phrasing still resolve correctly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;pattern()&lt;/code&gt; doesn't replace other variable methods — it sits next to it, for cases where flexibility matters more than precision.&lt;/p&gt;
&lt;h3 id=&quot;the-syntax&quot;&gt;The syntax&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;pattern(threshold: &quot;description&quot;, `
  css to inject
`)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;threshold&lt;/strong&gt; — a number from 0 to 1. The minimum similarity score a phrase must reach to trigger this pattern. Omit it and it defaults to &lt;code&gt;1&lt;/code&gt; (near-exact match only).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;description&lt;/strong&gt; — the plain-language phrase you're matching against.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;css&lt;/strong&gt; — what gets injected when a phrase matches. Use backticks so the CSS can freely contain single or double quotes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Threshold uses a colon, deliberately — &lt;code&gt;0.5:&lt;/code&gt; reads as a labeled parameter, distinct from a plain comma-separated argument. It's there so the number's &lt;em&gt;purpose&lt;/em&gt; is visually obvious to anyone reading your stylesheet.&lt;/p&gt;
&lt;h3 id=&quot;how-matching-works&quot;&gt;How matching works&lt;/h3&gt;
&lt;p&gt;Every phrase in your code gets compared against every pattern description using similarity scoring (Dice coefficient, if you're curious — it compares word overlap between the phrase and the description). The pattern with the &lt;strong&gt;highest score that also clears its own threshold&lt;/strong&gt; wins.&lt;/p&gt;
&lt;p&gt;That means:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;pattern(0.8: &quot;dark background and light color&quot;, `
  background: #212121;
  color: #ffffff;
`)

.chip {
  style dark background and light color
}

.tooltip {
  dark background light color
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both &lt;code&gt;.chip&lt;/code&gt; and &lt;code&gt;.tooltip&lt;/code&gt; resolve to the same CSS, even though neither phrase matches the description word-for-word. That's the point — you're not looking up a name, you're describing an outcome.&lt;/p&gt;
&lt;h4 id=&quot;tuning-the-threshold&quot;&gt;Tuning the threshold&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;High threshold (0.8–1)&lt;/strong&gt; → strict. Good for short, generic descriptions where you don't want accidental matches (&lt;code&gt;&quot;button&quot;&lt;/code&gt; alone would be too easy to trigger by accident).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Low threshold (0.4–0.6)&lt;/strong&gt; → forgiving. Good for longer, more specific descriptions where a few words might vary but the intent is clearly the same.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If two patterns could plausibly match the same phrase, the highest-scoring one wins — so keep descriptions distinct from each other, especially at lower thresholds.&lt;/p&gt;
&lt;h3 id=&quot;property-patterns-vs-block-patterns&quot;&gt;Property patterns vs. block patterns&lt;/h3&gt;
&lt;p&gt;Same split as &lt;code&gt;@define&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Property patterns&lt;/strong&gt; inject a group of declarations inside a rule:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;pattern(0.6: &quot;rounded primary button&quot;, `
  border: none;
  border-radius: 8px;
  padding: 10px 20px;
  background: #ffffff;
  color: #764ba2;
  font-weight: 600;
  cursor: pointer;
`)

.btn {
  rounded primary button
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Block patterns&lt;/strong&gt; inject full structures — keyframes, media queries, whole at-rules — and can stand alone outside any selector:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;pattern(0.7: &quot;Animated keyframe for spin&quot;, `
  @keyframes spin {
    0% { transform: rotate(0); }
    100% { transform: rotate(360deg); }
  }
`)

An Animated keyframe for spin
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;content-injection-example&quot;&gt;Content injection example&lt;/h3&gt;
&lt;p&gt;Because the CSS argument is just a string, &lt;code&gt;pattern()&lt;/code&gt; isn't limited to properties — it can inject anything valid at that point in your stylesheet, including pseudo-element content:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;pattern(0.9: &quot;hello world heading&quot;, `h1:before{content: 'Hello World';}`)

  hello world heading

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-s-shipped-in-v1-1-25&quot;&gt;What's shipped in v1.1.25&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;CDN&lt;/li&gt;
&lt;li&gt;API&lt;/li&gt;
&lt;li&gt;CLI&lt;/li&gt;
&lt;li&gt;VSCode Extention&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;try-it-3&quot;&gt;Try it&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pattern()&lt;/code&gt; is available on &lt;strong&gt;v1.1.25&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://fscss.devtem.org/pattern&quot;&gt;fscss.devtem.org/pattern&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/24831/pattern-in-fscss-1-1-25</guid>
<pubDate>Tue, 18 Aug 2026 12:18:46 +0000</pubDate>
</item>
<item>
<title>CSS Animations for Beginners: Transitions, Keyframes &amp; Transforms</title>
<link>https://coderlegion.com/23001/css-animations-for-beginners-transitions-keyframes-transforms</link>
<description>&lt;h2 id=&quot;introduction-1&quot;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;CSS animations were the thing that made me actually enjoy frontend development. Building a static layout is satisfying, but the moment something moves smoothly on hover or fades in on load, a project suddenly feels alive.&lt;/p&gt;
&lt;p&gt;The good news: you only need three concepts to cover almost every animation you'll want to build — transitions, transforms, and keyframes. Let's go through all three.&lt;/p&gt;
&lt;h2 id=&quot;1-transitions-smoothing-out-state-changes&quot;&gt;1. Transitions — Smoothing Out State Changes&lt;/h2&gt;
&lt;p&gt;A transition smooths out a property change between two states (like normal and hover).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.button {
  background-color: #3b82f6;
  transition: background-color 0.3s ease;
}

.button:hover {
  background-color: #2563eb;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without &lt;strong&gt;transition&lt;/strong&gt;, the color change would be instant. With it, the color smoothly shifts over 0.3 seconds.&lt;/p&gt;
&lt;p&gt;Syntax breakdown: &lt;strong&gt;transition: [property] [duration] [easing];&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In Tailwind:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;button class=&quot;bg-blue-500 hover:bg-blue-600 transition duration-300&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;2-transform-moving-scaling-rotating&quot;&gt;2. Transform — Moving, Scaling, Rotating&lt;/h2&gt;
&lt;p&gt;transform changes an element's position, size, or rotation without affecting the layout of surrounding elements.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.card {
  transition: transform 0.3s ease;
}

.card:hover {
  transform: translateY(-8px) scale(1.02);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This lifts a card slightly and grows it a tiny bit on hover — a very common effect on portfolio sites and product cards.&lt;/p&gt;
&lt;h2 id=&quot;common-transform-values-&quot;&gt;Common transform values:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;translateY(-8px)&lt;/strong&gt; — moves up&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;scale(1.05)&lt;/strong&gt; — grows by 5%&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;rotate(5deg)&lt;/strong&gt; — rotates slightly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;translateX(20px)&lt;/strong&gt; — moves right&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;In Tailwind:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;transition hover:-translate-y-2 hover:scale-105&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;3-keyframes-multi-step-animations&quot;&gt;3. Keyframes — Multi-Step Animations&lt;/h2&gt;
&lt;p&gt;Transitions handle two states. &lt;strong&gt;@keyframes&lt;/strong&gt; handles animations with multiple steps, or ones that run automatically without needing a trigger like &lt;strong&gt;:hover&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.hero-title {
  animation: fadeInUp 0.8s ease-out;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes an element fade in while sliding up slightly — a very common &quot;page load&quot; effect.&lt;/p&gt;
&lt;p&gt;You can also define multiple steps with percentages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@keyframes pulse {
  0% { transform: scale(1); }
  50% { transform: scale(1.1); }
  100% { transform: scale(1); }
}

.notification-dot {
  animation: pulse 2s infinite;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a continuous pulsing effect, useful for notification badges or &quot;live&quot; indicators.&lt;/p&gt;
&lt;h2 id=&quot;putting-it-together-a-real-example&quot;&gt;Putting It Together: A Real Example&lt;/h2&gt;
&lt;p&gt;Here's a button with a hover lift, a smooth color transition, and a subtle entrance animation on page load:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

.cta-button {
  background-color: #3b82f6;
  transition: all 0.3s ease;
  animation: fadeIn 0.6s ease-in;
}

.cta-button:hover {
  background-color: #2563eb;
  transform: translateY(-4px);
  box-shadow: 0 10px 20px rgba(59, 130, 246, 0.3);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is genuinely how most polished-feeling buttons on real websites are built — nothing exotic, just transitions and transforms combined thoughtfully.&lt;/p&gt;
&lt;h2 id=&quot;common-beginner-mistakes&quot;&gt;Common Beginner Mistakes&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Animating too many properties at once. Subtlety wins — one or two properties changing smoothly looks more professional than five things moving at once.&lt;/li&gt;
&lt;li&gt;Using &lt;strong&gt;all&lt;/strong&gt; in transitions carelessly. &lt;strong&gt;transition: all 0.3s&lt;/strong&gt; works, but is slightly less performant than specifying exact properties like &lt;strong&gt;transition: transform 0.3s, background-color 0.3s&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Animating &lt;strong&gt;width/height/top/left&lt;/strong&gt; instead of &lt;strong&gt;transform&lt;/strong&gt;. These properties trigger layout recalculation and can feel janky. &lt;strong&gt;transform&lt;/strong&gt; and &lt;strong&gt;opacity&lt;/strong&gt; are GPU-accelerated and much smoother.&lt;/li&gt;
&lt;li&gt;Overusing &lt;strong&gt;infinite&lt;/strong&gt; animations. A pulsing badge is fine. A constantly bouncing hero heading gets annoying fast — use continuous animation sparingly.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;final-thoughts-2&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;You don't need a JavaScript animation library for most of what makes a website feel polished — smooth hovers, subtle lifts, and clean entrance animations are all achievable with plain CSS. Start by adding a transition to every hover state in your next project and see how much more &quot;finished&quot; it instantly feels.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Muhammad Farhan&lt;/strong&gt; is a Frontend Developer specializing in React.js and Tailwind CSS, based in Dera Ismail Khan, Pakistan.&lt;br&gt;
&lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://muhammadfarhandev.netlify.app&quot;&gt;Muhammad Farhan&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23001/css-animations-for-beginners-transitions-keyframes-transforms</guid>
<pubDate>Tue, 11 Aug 2026 05:00:00 +0000</pubDate>
</item>
<item>
<title>I built pure CSS 3D dice cube module: FSCSS</title>
<link>https://coderlegion.com/24274/i-built-pure-css-3d-dice-cube-module-fscss</link>
<description>&lt;p&gt;&lt;code&gt;cube.fscss&lt;/code&gt; is a lightweight, pure CSS 3D cube system built on &lt;a rel=&quot;nofollow&quot; href=&quot;https://fscss.devtem.org/&quot;&gt;FSCSS&lt;/a&gt;. It renders a spinnable 6-face cube using nothing but perspective, transform-style: preserve-3d, and translateZ/rotateX/rotateY — no JavaScript, no 3D library, no canvas.&lt;/p&gt;
&lt;p&gt;Here's example whole thing rendered live, nothing but &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;s and CSS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/fscss@1.1.24/exec.min.js&quot; defer&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;style&amp;gt;
@import((*) from cube)

@cube-root()
@cube-wrapper(.my-cube)
@cube-face(.my-cube div)
@cube-face-front(.my-cube .f1)
@cube-face-back(.my-cube .f2)
@cube-face-left(.my-cube .f3)
@cube-face-right(.my-cube .f4)
@cube-face-top(.my-cube .f5)
@cube-face-bottom(.my-cube .f6)
@cube-micro-dice(.my-cube .cube)

.my-cube {
  @cube-size(80px)
  transform: rotateY(50deg) rotateX(30deg);
  animation: spin 5s linear infinite;
}

@keyframes spin {
  0%   { transform: rotateX(20deg) rotateY(40deg); }
  100% { transform: rotateX(380deg) rotateY(400deg); }
}
&amp;lt;/style&amp;gt;

&amp;lt;div class=&quot;my-cube&quot;&amp;gt;
  &amp;lt;div class=&quot;cube f1 dice-1&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;cube f2 dice-2&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;cube f3 dice-3&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;cube f4 dice-4&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;cube f5 dice-5&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;cube f6 dice-6&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No dice PNGs. No SVG paths. No canvas draw calls. Every pip is a &lt;code&gt;radial-gradient&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;why-build-this&quot;&gt;Why build this&lt;/h3&gt;
&lt;p&gt;Already had a chart/dashboard module (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fscss-ttr/st-core.fscss&quot;&gt;st-core.fscss&lt;/a&gt;) built on &lt;code&gt;clip-path&lt;/code&gt; polygons — no JS charting library, just CSS custom properties driving the shape. I wanted to see how far that &quot;CSS does the visual math&quot; philosophy could stretch into actual 3D space. A cube felt like the right next test: real &lt;code&gt;perspective&lt;/code&gt;, real &lt;code&gt;preserve-3d&lt;/code&gt;, real face geometry — not a flat illusion.&lt;/p&gt;
&lt;h3 id=&quot;how-the-geometry-works&quot;&gt;How the geometry works&lt;/h3&gt;
&lt;p&gt;A cube is 6 absolutely-positioned faces sharing one parent set to &lt;code&gt;transform-style: preserve-3d&lt;/code&gt;. Each face gets pushed out from the center by half the cube's edge length, then rotated to face outward:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;front  → translateZ(half)
back   → rotateY(180deg) translateZ(half)
top    → rotateX(90deg)  translateZ(half)
bottom → rotateX(-90deg) translateZ(half)
right  → rotateY(90deg)  translateZ(half)
left   → rotateY(-90deg) translateZ(half)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The entire cube's dimensions live in two CSS variables: &lt;code&gt;--cube-size&lt;/code&gt; and &lt;code&gt;--cube-half&lt;/code&gt; (always size ÷ 2). Resize the whole thing with one call:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.my-cube {
  @cube-size(150px)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Faces, offsets, and dice dot size all scale together — no manually recalculating six &lt;code&gt;translateZ&lt;/code&gt; values by hand.&lt;/p&gt;
&lt;h3 id=&quot;dice-pips-no-font-glyphs&quot;&gt;Dice pips, no font glyphs&lt;/h3&gt;
&lt;p&gt;I originally reached for Unicode dice characters (⚀⚁⚂⚃⚄⚅) for the pips, but font rendering for those glyphs is wildly inconsistent across platforms — some Android/Chrome stacks show them thin, outlined, or as tofu boxes. Stacked &lt;code&gt;radial-gradient&lt;/code&gt;s solved it instead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.dice-3 {
  background-image:
    radial-gradient(circle var(--cube-dot-size) at 25% 25%, var(--cube-dot-color) 99%, transparent 100%),
    radial-gradient(circle var(--cube-dot-size) at 50% 50%, var(--cube-dot-color) 99%, transparent 100%),
    radial-gradient(circle var(--cube-dot-size) at 75% 75%, var(--cube-dot-color) 99%, transparent 100%);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Font-independent, crisp at any size, and the dot radius is itself a variable — so it scales automatically with &lt;code&gt;@cube-size()&lt;/code&gt; instead of overflowing on a big cube or vanishing on a small one.&lt;/p&gt;
&lt;h3 id=&quot;it-s-not-just-dice&quot;&gt;It's not just dice&lt;/h3&gt;
&lt;p&gt;Faces and face &lt;em&gt;content&lt;/em&gt; are decoupled on purpose. The face mixins only handle position — what renders inside is a separate &quot;micro&quot; you apply on top. Right now there are eight:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cube-micro-fire&lt;/code&gt; — pulsing gradient-text fire&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cube-micro-matrix&lt;/code&gt; — scrolling digital rain&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cube-micro-neon&lt;/code&gt; — pulsing cyan/magenta glow border&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cube-micro-float&lt;/code&gt; — subtle bounce, stacks with anything&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cube-micro-glass&lt;/code&gt; — frosted &lt;code&gt;backdrop-filter&lt;/code&gt; panel&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cube-micro-holo&lt;/code&gt; — holographic shifting gradient text&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cube-micro-glitch&lt;/code&gt; — RGB-split glitch shift&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cube-micro-dice&lt;/code&gt; — the pips above&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mix and match per face on the same cube:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@cube-micro-fire(.my-cube .f1)
@cube-micro-matrix(.my-cube .f2)
@cube-micro-neon(.my-cube .f3)
@cube-micro-glass(.my-cube .f5)
@cube-micro-holo(.my-cube .f6)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;try-it-4&quot;&gt;Try it&lt;/h3&gt;
&lt;p&gt;CDN, drop-in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/fscss@1.1.24/exec.min.js&quot; defer&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;style&amp;gt; 
  @import((*) from cube)
  /* your mixins here */
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or compile ahead of time with the CLI for a plain &lt;code&gt;.css&lt;/code&gt; file, zero runtime:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -g fscss
fscss style.fscss style.css
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Full docs, token reference, and more examples: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fscss-ttr/cube.fscss&quot;&gt;github.com/fscss-ttr/cube.fscss&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you build something with it — I'd love to see. And if you hit issue, open an issue, it's open-source. &lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;help us star the repo so more dev can discover it. 🙏&lt;/em&gt;&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/24274/i-built-pure-css-3d-dice-cube-module-fscss</guid>
<pubDate>Fri, 07 Aug 2026 22:17:10 +0000</pubDate>
</item>
<item>
<title>Why I Replaced Tailwind CSS With Vanilla CSS</title>
<link>https://coderlegion.com/24214/why-i-replaced-tailwind-css-with-vanilla-css</link>
<description>&lt;p&gt;I removed Tailwind CSS from this site and rewrote every style by hand in vanilla CSS. The reason is short: modern CSS now does natively what I was pulling Tailwind in to get. Cascade layers, custom properties, nesting, and &lt;code&gt;:has()&lt;/code&gt; all ship in every current browser, and Tailwind v4 is built on those exact primitives. Once the browser speaks the language directly, a build step that generates it for you reads as overhead rather than leverage. When I &lt;a rel=&quot;nofollow&quot; href=&quot;https://morello.dev/blog/the-new-website&quot;&gt;rebuilt this site in December 2025&lt;/a&gt; I reached for &lt;a rel=&quot;nofollow&quot; href=&quot;https://tailwindcss.com&quot;&gt;Tailwind v4&lt;/a&gt; on instinct. This month I took it back out.&lt;/p&gt;
&lt;h3 id=&quot;tl-dr&quot;&gt;TL;DR&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The features I used Tailwind for (design tokens, nesting, scoping, a consistent scale) are now native CSS or handled by Astro's scoped styles.&lt;/li&gt;
&lt;li&gt;Tailwind v4 is itself built on native cascade layers, custom properties, and &lt;code&gt;color-mix()&lt;/code&gt;, with &lt;a rel=&quot;nofollow&quot; href=&quot;https://lightningcss.dev&quot;&gt;Lightning CSS&lt;/a&gt; doing the build. I was shipping an engine to generate CSS the browser already understands.&lt;/li&gt;
&lt;li&gt;The migration dropped three Tailwind packages, moved global styles into native &lt;code&gt;@layer&lt;/code&gt;s, and put every component's styles in Astro's scoped &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; blocks.&lt;/li&gt;
&lt;li&gt;The site stayed &lt;a rel=&quot;nofollow&quot; href=&quot;https://web.dev/baseline&quot;&gt;Baseline Widely available&lt;/a&gt; the whole way.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;why-replace-tailwind-at-all-&quot;&gt;Why replace Tailwind at all?&lt;/h3&gt;
&lt;p&gt;Because the job Tailwind did for me is now built into the platform. When I list what I actually reached for it to solve, every item has a native answer today:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;What I used Tailwind for                 &lt;/th&gt;&lt;th&gt;The vanilla replacement                   &lt;/th&gt;&lt;th&gt;Baseline Widely since&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Style layering and predictable overrides &lt;/td&gt;&lt;td&gt;&lt;code&gt;@layer&lt;/code&gt; cascade layers                   &lt;/td&gt;&lt;td&gt;Sept 2024&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Design tokens and theming                &lt;/td&gt;&lt;td&gt;Custom properties (&lt;code&gt;--x&lt;/code&gt;)                 &lt;/td&gt;&lt;td&gt;long-standing&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Nesting without a preprocessor           &lt;/td&gt;&lt;td&gt;Native CSS nesting (&lt;code&gt;&amp;amp;&lt;/code&gt;)                  &lt;/td&gt;&lt;td&gt;June 2026&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Parent and state selectors               &lt;/td&gt;&lt;td&gt;&lt;code&gt;:has()&lt;/code&gt;                                  &lt;/td&gt;&lt;td&gt;June 2026&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Component-scoped responsive rules        &lt;/td&gt;&lt;td&gt;Container queries and Astro scoped styles &lt;/td&gt;&lt;td&gt;Aug 2025&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The dates are the point. &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting&quot;&gt;CSS nesting&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/:has&quot;&gt;&lt;code&gt;:has()&lt;/code&gt;&lt;/a&gt; only crossed into Baseline Widely availability around June 2026, roughly two months before I did this. &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@layer&quot;&gt;Cascade layers&lt;/a&gt; got there in September 2024, &lt;code&gt;color-mix()&lt;/code&gt; in November 2025, size container queries in August 2025. The Chrome team's &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.chrome.com/blog/css-wrapped-2025&quot;&gt;CSS Wrapped 2025&lt;/a&gt; counted 22 new CSS features landing in Chrome in a single year. The gap Tailwind was filling closed while I wasn't looking, and 2026 is the year it actually finished closing.&lt;/p&gt;
&lt;p&gt;None of this makes Tailwind bad. It makes it optional for what I'm doing here: a single-author content site where nobody but me touches the CSS.&lt;/p&gt;
&lt;h3 id=&quot;tailwind-v4-already-runs-on-the-css-i-switched-to&quot;&gt;Tailwind v4 already runs on the CSS I switched to&lt;/h3&gt;
&lt;p&gt;The thing that tipped me over was reading how &lt;a rel=&quot;nofollow&quot; href=&quot;https://tailwindcss.com/blog/tailwindcss-v4&quot;&gt;Tailwind v4&lt;/a&gt; works under the hood. Its January 2025 rewrite is explicit that it is built on modern CSS. The announcement lists &quot;native cascade layers,&quot; &quot;registered custom properties,&quot; &quot;&lt;code&gt;color-mix()&lt;/code&gt;,&quot; and logical properties as the foundation. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://tailwindcss.com/blog/tailwindcss-v4-alpha&quot;&gt;earlier post introducing its Oxide engine&lt;/a&gt; is blunter about the plumbing: the engine is &quot;up to 10x faster,&quot; and &quot;the only thing the new engine depends on is Lightning CSS.&quot;&lt;/p&gt;
&lt;p&gt;Read that back with a migration in mind. Tailwind v4 generates native cascade layers, sets custom properties, and runs the output through Lightning CSS for prefixing and minification. That is the same stack I now write directly. I was running an engine whose job was to emit CSS features my target browsers already implement. For an app with dozens of engineers and a shared design system, that engine buys you consistency and guardrails worth paying for. For this site, it was a generator sitting between me and CSS I could just write.&lt;/p&gt;
&lt;p&gt;There is a small irony worth stating plainly. Tailwind v4 leans on &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@property&quot;&gt;registered custom properties&lt;/a&gt; (&lt;code&gt;@property&lt;/code&gt;), which won't reach Baseline Widely availability until around January 2027, so Tailwind ships its own fallbacks for it. My hand-written CSS holds a stricter bar than the framework I removed.&lt;/p&gt;
&lt;h3 id=&quot;what-the-migration-actually-looked-like&quot;&gt;What the migration actually looked like&lt;/h3&gt;
&lt;p&gt;The site is &lt;a rel=&quot;nofollow&quot; href=&quot;https://astro.build&quot;&gt;Astro&lt;/a&gt;, so it splits cleanly in two: global styles in &lt;code&gt;src/styles&lt;/code&gt;, and per-component styles in each &lt;code&gt;.astro&lt;/code&gt; file's scoped &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; block. I rewrote both.&lt;/p&gt;
&lt;h4 id=&quot;cascade-layers-replace-the-tailwind-import&quot;&gt;Cascade layers replace the Tailwind import&lt;/h4&gt;
&lt;p&gt;The old entry point started with Tailwind's import, its typography plugin, and a &lt;code&gt;@custom-variant&lt;/code&gt; line for every palette so theme selectors would compile:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@import &quot;tailwindcss&quot;;
@plugin &quot;@tailwindcss/typography&quot;;

@custom-variant tokyo-night (&amp;amp;:where([data-theme=&quot;tokyo-night&quot;]));
@custom-variant dracula (&amp;amp;:where([data-theme=&quot;dracula&quot;]));
/* ...thirteen more palettes... */
@custom-variant dark (&amp;amp;:where([data-mode=&quot;dark&quot;], [data-mode=&quot;dark&quot;] *));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fifteen &lt;code&gt;@custom-variant&lt;/code&gt; declarations, one per palette, plus one for dark mode, all to teach the framework about selectors the browser already understands. The replacement is one line of real CSS that declares the cascade order, then a plain file per layer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@layer reset, tokens, base, primitives, prose, utilities;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ordering the layers by hand turned out to be the feature I most underrated in Tailwind. Layers win by declaration order regardless of selector specificity, so the reset can be dead simple, tokens can sit under everything, and utilities win last without a single &lt;code&gt;!important&lt;/code&gt;. I get the exact override behavior Tailwind gave me, spelled out in one readable statement.&lt;/p&gt;
&lt;h4 id=&quot;design-tokens-are-just-custom-properties&quot;&gt;Design tokens are just custom properties&lt;/h4&gt;
&lt;p&gt;Theming didn't really change, which is the tell. Under Tailwind the palettes were already CSS variables swapped by a &lt;code&gt;data-theme&lt;/code&gt; attribute; the framework just wrapped them in &lt;code&gt;@theme&lt;/code&gt; and &lt;code&gt;@custom-variant&lt;/code&gt;. Stripping that wrapper left plain rules:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:root,
[data-theme=&quot;catppuccin-mocha&quot;] {
  --bg: #11111b;
  --ink: #cdd6f4;
  --accent: #89b4fa;
  /* ...the rest of the reduced token set... */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sixteen of those blocks, one per theme, in a &lt;code&gt;tokens&lt;/code&gt; layer. The switcher sets &lt;code&gt;data-theme&lt;/code&gt; on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;, the variables cascade in, and syntax highlighting tracks them through more variables. No config object, no plugin API, just the custom properties that were doing the work all along.&lt;/p&gt;
&lt;h4 id=&quot;components-from-class-lists-to-scoped-styles&quot;&gt;Components: from class lists to scoped styles&lt;/h4&gt;
&lt;p&gt;This is where the day-to-day difference shows. Here is the card component before, as a Tailwind class list:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;&amp;lt;article
  class:list={[
    &quot;relative rounded border bg-card p-4 sm:p-6&quot;,
    &quot;has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-ring&quot;,
    className,
  ]}
  {...props}
&amp;gt;
  &amp;lt;slot /&amp;gt;
&amp;lt;/article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And after, as scoped CSS in the same file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;&amp;lt;article class={className} {...props}&amp;gt;
  &amp;lt;slot /&amp;gt;
&amp;lt;/article&amp;gt;

&amp;lt;style&amp;gt;
  article {
    position: relative;
    border-radius: var(--radius);
    border-width: 1px;
    background-color: var(--bg-elevated);
    padding: 1rem;

    &amp;amp;:has(:focus-visible) {
      outline: 2px solid var(--accent);
      outline-offset: 2px;
    }
  }

  @media (width &amp;gt;= 40rem) {
    article {
      padding: 1.5rem;
    }
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The markup goes quiet and the intent moves into CSS that reads like CSS. Native nesting holds the &lt;code&gt;:has()&lt;/code&gt; state rule next to the base rule, and a normal media query handles the responsive padding that &lt;code&gt;sm:p-6&lt;/code&gt; used to encode. &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.astro.build/en/guides/styling/#scoped-styles&quot;&gt;Astro scopes these styles&lt;/a&gt; through a generated &lt;code&gt;data-astro-cid&lt;/code&gt; attribute, so a bare &lt;code&gt;article&lt;/code&gt; selector is enough and the element needs no class of its own. And because scoped styles compile unlayered, they beat the global layers without any specificity games. That last property is what made the migration safe to do component by component: a scoped rule always wins over a global primitive, so I could move one file at a time without the two systems fighting.&lt;/p&gt;
&lt;h4 id=&quot;lightning-css-does-the-vendor-work&quot;&gt;Lightning CSS does the vendor work&lt;/h4&gt;
&lt;p&gt;The reflex worry with dropping a framework is that you inherit its chores: prefixing, downleveling new syntax, minifying. I inherited none of them, because &lt;a rel=&quot;nofollow&quot; href=&quot;https://lightningcss.dev&quot;&gt;Lightning CSS&lt;/a&gt; was already in the pipeline. It's a Rust parser, transformer, and minifier by &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/devongovett&quot;&gt;Devon Govett&lt;/a&gt;, and it is the same tool Tailwind v4's Oxide engine depends on. Astro's Rust compiler &lt;a rel=&quot;nofollow&quot; href=&quot;https://morello.dev/blog/astro-7&quot;&gt;uses it too&lt;/a&gt;, so turning it on for my own CSS was one line:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;vite: {
  css: { transformer: &quot;lightningcss&quot; },
  build: {
    cssTarget: [&quot;chrome111&quot;, &quot;edge111&quot;, &quot;firefox114&quot;, &quot;safari16.4&quot;, &quot;ios16.4&quot;],
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It adds the handful of &lt;code&gt;-webkit-&lt;/code&gt; prefixes my target browsers still need, lowers nesting for the stragglers, and minifies, all against a Baseline target set. The &lt;code&gt;package.json&lt;/code&gt; diff was the satisfying part: out went &lt;code&gt;tailwindcss&lt;/code&gt;, &lt;code&gt;@tailwindcss/vite&lt;/code&gt;, and &lt;code&gt;@tailwindcss/typography&lt;/code&gt;; in went a single &lt;code&gt;lightningcss&lt;/code&gt; dev dependency that was effectively already there. The long-form article styling followed the same instinct I wrote about in &lt;a rel=&quot;nofollow&quot; href=&quot;https://morello.dev/blog/shadcn-typeset-vs-tailwind-prose&quot;&gt;shadcn/typeset vs Tailwind Typography&lt;/a&gt;: own the file, delete the rules you don't want.&lt;/p&gt;
&lt;h3 id=&quot;did-the-site-stay-baseline-compliant-&quot;&gt;Did the site stay Baseline compliant?&lt;/h3&gt;
&lt;p&gt;Yes, and that was a hard constraint, not a nice-to-have. Every feature this site ships has to be &lt;a rel=&quot;nofollow&quot; href=&quot;https://morello.dev/blog/baseline-2026-web-platform-apis&quot;&gt;Baseline Widely available&lt;/a&gt;, interoperable across the core browsers for at least 30 months. That's exactly why the timing worked: the two features I lean on hardest, native nesting and &lt;code&gt;:has()&lt;/code&gt;, only reached Widely availability around June 2026. If I'd tried this a year earlier they'd have been Baseline Newly at best, and the card component above would have needed an &lt;code&gt;@supports&lt;/code&gt; gate with a full fallback. Doing it now, they're safe to use unguarded. The one place I still gate behind &lt;code&gt;@supports&lt;/code&gt; is the anchor-positioned reading indicator in the table of contents, which is genuinely still Newly available.&lt;/p&gt;
&lt;h3 id=&quot;do-you-still-need-tailwind-in-2026-&quot;&gt;Do you still need Tailwind in 2026?&lt;/h3&gt;
&lt;p&gt;Only if someone other than you owns the CSS. That is the honest split, and it decides whether replacing Tailwind with vanilla CSS is worth it for you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A team with a shared design system:&lt;/strong&gt; probably keep Tailwind. The constraints that feel like overhead on a solo site are the whole point when twenty people style the same product. Consistency you don't have to enforce by review is worth a build step.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A solo project or a content site:&lt;/strong&gt; the trade-off has flipped. You get design tokens from custom properties, override control from cascade layers, nesting and state selectors natively, and scoping from your framework's components. The framework's value is real but no longer necessary.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'm not claiming Tailwind lost. &lt;a rel=&quot;nofollow&quot; href=&quot;https://kevinpowell.co&quot;&gt;Kevin Powell&lt;/a&gt; has spent years arguing that learning CSS itself pays off, and the platform finally caught up to make that cheap. Tailwind v4 is a genuinely good piece of engineering, built on the same modern CSS that made me comfortable leaving it. For this site, hand-written CSS wired to theme variables is less code, one fewer dependency, and output I can read top to bottom. That's the same own-the-source call I keep making here, and this time the browser did most of the work for me.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24214/why-i-replaced-tailwind-css-with-vanilla-css</guid>
<pubDate>Thu, 06 Aug 2026 12:39:32 +0000</pubDate>
</item>
<item>
<title>Baseline 2026: 4 APIs That Changed How I Code</title>
<link>https://coderlegion.com/23275/baseline-2026-4-apis-that-changed-how-i-code</link>
<description>&lt;p&gt;Baseline 2026 brought a wave of new interoperable web platform APIs. Not the kind of big where everything changes overnight. The kind where APIs that have been experimental for years finally ship in every browser. Baseline means you can use them in production without a polyfill, and the 2026 cohort has a handful that actually changed what I reach for when I start a new project. Not &quot;technically you could,&quot; but &quot;I stopped installing this.&quot;&lt;/p&gt;
&lt;h3 id=&quot;the-navigation-api-one-navigate-event-instead-of-three-different-apis&quot;&gt;The Navigation API: one &lt;code&gt;navigate&lt;/code&gt; event instead of three different APIs&lt;/h3&gt;
&lt;p&gt;The Navigation API &lt;a rel=&quot;nofollow&quot; href=&quot;https://web.dev/blog/baseline-navigation-api&quot;&gt;became Baseline Newly available in early 2026&lt;/a&gt;, supported in Chrome, Edge, Firefox 147, and Safari 26.2. It replaces the fragmented mess that was History API routing: &lt;code&gt;pushState&lt;/code&gt; for navigation, &lt;code&gt;popstate&lt;/code&gt; for back/forward, and manual link click handlers to prevent full-page reloads. Three separate APIs you had to wire together yourself, or reach for a router library that did it for you.&lt;/p&gt;
&lt;p&gt;Here is what client-side routing looked like before:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Before: three separate concerns, manually wired
window.addEventListener(&quot;popstate&quot;, (e) =&amp;gt; {
  renderRoute(window.location.pathname);
});

document.addEventListener(&quot;click&quot;, (e) =&amp;gt; {
  const link = e.target.closest(&quot;a[data-route]&quot;);
  if (link) {
    e.preventDefault();
    history.pushState(null, &quot;&quot;, link.href);
    renderRoute(new URL(link.href).pathname);
  }
});

// Initial render
renderRoute(window.location.pathname);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here is the Navigation API equivalent:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// After: one centralized event
navigation.addEventListener(&quot;navigate&quot;, (e) =&amp;gt; {
  const url = new URL(e.destination.url);
  e.intercept({
    handler() {
      renderRoute(url.pathname);
    },
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One event. One interception point. The &lt;code&gt;intercept()&lt;/code&gt; call tells the browser &quot;I'll handle this navigation, wait for me,&quot; which also means the browser can show a loading indicator natively. You get scroll restoration (&lt;code&gt;e.scroll()&lt;/code&gt;), form data access (&lt;code&gt;e.formData&lt;/code&gt;), and entry traversal (&lt;code&gt;navigation.traverseTo(key)&lt;/code&gt;) without wiring any of it yourself.&lt;/p&gt;
&lt;p&gt;This does not kill React Router or TanStack Router. Both have open discussions about adopting the Navigation API as a backend: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/remix-run/react-router/discussions/11046&quot;&gt;React Router's&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/TanStack/router/discussions/821&quot;&gt;TanStack Router's&lt;/a&gt;. They would sit on top of it rather than reimplement the History API workaround they currently maintain. The frameworks add value (nested routes, data loading, error boundaries) that the raw Navigation API does not. What the API kills is the need to pull in a router &lt;em&gt;just&lt;/em&gt; to avoid full-page reloads on a content site. If your routing needs are modest (a blog, a docs site, a dashboard with a few views), the platform now has you covered.&lt;/p&gt;
&lt;p&gt;The caveat: Safari 26.2 is &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.infoq.com/news/2026/05/navigation-api-browser&quot;&gt;missing &lt;code&gt;precommitHandler&lt;/code&gt;&lt;/a&gt; support, which limits some advanced interception patterns. And Ian Hickson, the spec author, &lt;a rel=&quot;nofollow&quot; href=&quot;https://html5doctor.com/interview-with-ian-hickson-html-editor&quot;&gt;famously called&lt;/a&gt; &lt;code&gt;pushState()&lt;/code&gt; his &quot;favourite mistake,&quot; so the replacement had a low bar to clear.&lt;/p&gt;
&lt;h3 id=&quot;container-style-queries-theming-without-js-class-toggling&quot;&gt;Container style queries: theming without JS class toggling&lt;/h3&gt;
&lt;p&gt;Container style queries for custom properties &lt;a rel=&quot;nofollow&quot; href=&quot;https://web.dev/blog/web-platform-05-2026&quot;&gt;became Baseline Newly available in May 2026&lt;/a&gt;, with Firefox 151 shipping the last piece and Chrome 148 adding name-only container queries the same month.&lt;/p&gt;
&lt;p&gt;This one solves a problem I have written far too much JavaScript for: switching visual variants based on context. A card that renders differently in a sidebar vs. a hero section. A theme toggle that propagates across the component tree. All done with CSS classes, JS observers, or both.&lt;/p&gt;
&lt;p&gt;Here is the old pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// Before: JS class toggling for a card variant
function Card({ variant = &quot;default&quot; }) {
  return (
    &amp;lt;div className={`card card--${variant}`}&amp;gt;
      &amp;lt;h3&amp;gt;{title}&amp;lt;/h3&amp;gt;
      &amp;lt;p&amp;gt;{excerpt}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.card--featured { /* special styles */ }
.card--compact { /* compact styles */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And with container style queries:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.sidebar {
  container-name: sidebar;
  /* no container-type needed for style queries */
}

.card {
  /* default styles */
}

@container sidebar style(--variant: featured) {
  .card {
    /* featured layout */
  }
}

@container sidebar style(--compact: true) {
  .card {
    /* compact layout */
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The container declares its context via CSS custom properties, and descendant elements react. No JS. No class chains. The component does not need to know where it is rendered; the container owns that relationship.&lt;/p&gt;
&lt;p&gt;The limitation worth mentioning: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/mdn/content/issues/44701&quot;&gt;style queries currently only support custom properties&lt;/a&gt;. The spec allows querying any CSS property, but no browser ships that yet. So you query &lt;code&gt;style(--theme: dark)&lt;/code&gt;, not &lt;code&gt;style(background-color: black)&lt;/code&gt;. For the use case it solves (theming, layout variants, contextual styling): custom properties are exactly what you want. For arbitrary CSS property introspection, we are still waiting.&lt;/p&gt;
&lt;h3 id=&quot;-open-state-tracking-you-never-write&quot;&gt;&lt;code&gt;:open&lt;/code&gt;: state tracking you never write&lt;/h3&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://web.dev/blog/web-platform-05-2026&quot;&gt;&lt;code&gt;:open&lt;/code&gt; pseudo-class became Baseline Newly available in May 2026&lt;/a&gt; when Safari 26.5 shipped it. It matches any element with an open semantic state: &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;input type=&quot;color&quot;&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;input type=&quot;date&quot;&amp;gt;&lt;/code&gt; with their pickers open.&lt;/p&gt;
&lt;p&gt;Before &lt;code&gt;:open&lt;/code&gt;, styling an open disclosure meant either an attribute selector or JS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* Before: attribute selector, limited */
details[open] &amp;gt; summary {
  border-radius: 4px 4px 0 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Before: manual state tracking for a dialog
dialog.addEventListener(&quot;toggle&quot;, () =&amp;gt; {
  document.body.classList.toggle(&quot;dialog-open&quot;, dialog.open);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With &lt;code&gt;:open&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;details:open &amp;gt; summary {
  border-radius: 4px 4px 0 0;
}

/* Style the page when any dialog is open */
html:has(dialog:open) {
  overflow: hidden;
}

/* Style a label when its associated select is open */
label:has(select:open) {
  color: var(--accent);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;:has()&lt;/code&gt; combo is where this gets useful. &lt;code&gt;html:has(dialog:open)&lt;/code&gt; is a body scroll lock in one line of CSS. &lt;code&gt;label:has(select:open)&lt;/code&gt; is a parent style change driven by a child's state: something that used to require a JS mutation observer or a framework binding. The &lt;code&gt;:open&lt;/code&gt; selector tracks semantic state, not visibility: a &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; that is semantically open but visually hidden still matches &lt;code&gt;:open&lt;/code&gt;. That distinction matters for things like &lt;code&gt;display: none&lt;/code&gt; inside an open disclosure.&lt;/p&gt;
&lt;p&gt;This replaces the &lt;code&gt;details[open]&lt;/code&gt; attribute selector (which only works for &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;) and the pattern of manually toggling CSS classes on &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; or parent elements when modals or pickers open. One pseudo-class, all openable elements, no JS.&lt;/p&gt;
&lt;h3 id=&quot;math-sumprecise-the-wait-reduce-is-wrong-moment&quot;&gt;&lt;code&gt;Math.sumPrecise()&lt;/code&gt;: the &quot;wait, &lt;code&gt;reduce&lt;/code&gt; is wrong?&quot; moment&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Math.sumPrecise()&lt;/code&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sumPrecise&quot;&gt;became Baseline Newly available in April 2026&lt;/a&gt;, part of ES2026 after &lt;a rel=&quot;nofollow&quot; href=&quot;https://socket.dev/blog/tc39-advances-11-proposals-for-math-precision-binary-apis-and-more&quot;&gt;reaching TC39 Stage 4&lt;/a&gt; in July 2025.&lt;/p&gt;
&lt;p&gt;The problem it solves is floating-point summation error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const numbers = [1e20, 0.1, -1e20];
numbers.reduce((a, b) =&amp;gt; a + b, 0);  // 0
Math.sumPrecise(numbers);              // 0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The naive &lt;code&gt;reduce&lt;/code&gt; approach loses precision because floating-point addition is not associative. When you add a very small number to a very large one, the small number disappears. &lt;code&gt;Math.sumPrecise()&lt;/code&gt; uses the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/tc39/proposal-math-sum&quot;&gt;Shewchuk algorithm&lt;/a&gt; (or an equivalent) to produce the maximally correct answer: the result you would get with arbitrary-precision arithmetic rounded back to a float.&lt;/p&gt;
&lt;p&gt;What it does NOT fix: &lt;code&gt;0.1 + 0.2&lt;/code&gt;. That is a floating-point &lt;em&gt;representation&lt;/em&gt; problem, not a summation problem. &lt;code&gt;Math.sumPrecise([0.1, 0.2])&lt;/code&gt; still returns &lt;code&gt;0.30000000000000004&lt;/code&gt; because the literals &lt;code&gt;0.1&lt;/code&gt; and &lt;code&gt;0.2&lt;/code&gt; are inexact before &lt;code&gt;sumPrecise&lt;/code&gt; ever sees them.&lt;/p&gt;
&lt;p&gt;The API is deliberately not variadic (&lt;code&gt;Math.sumPrecise(1, 2, 3)&lt;/code&gt; throws). It takes an iterable to avoid stack overflows on large datasets. Empty iterables return &lt;code&gt;-0&lt;/code&gt; (the floating-point additive identity). Non-number elements throw a &lt;code&gt;TypeError&lt;/code&gt;, unlike &lt;code&gt;Math.max&lt;/code&gt; which silently coerces. TypeScript does not ship type definitions for it yet (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/microsoft/TypeScript/issues/63427&quot;&gt;issue #63427&lt;/a&gt;), so you may need a manual declaration depending on your &lt;a rel=&quot;nofollow&quot; href=&quot;https://morello.dev/blog/typescript-7-is-here&quot;&gt;TypeScript 7&lt;/a&gt; version.&lt;/p&gt;
&lt;p&gt;This is not the API that will headline anyone's year-end retrospective. But it fixes a real footgun: the kind of bug that passes code review because nobody spots the summation error in a 50-item array spread across a few chained &lt;code&gt;.map()&lt;/code&gt; and &lt;code&gt;.filter()&lt;/code&gt; calls. I wrote about &lt;a rel=&quot;nofollow&quot; href=&quot;https://morello.dev/blog/five-things-you-might-not-know-about-javascript&quot;&gt;JavaScript gotchas&lt;/a&gt; before, and floating-point behavior is the category that surprises experienced developers the most. Having a built-in that handles the common case correctly is a quiet win.&lt;/p&gt;
&lt;h3 id=&quot;what-baseline-newly-available-actually-means-for-baseline-2026&quot;&gt;What &quot;Baseline Newly available&quot; actually means for Baseline 2026&lt;/h3&gt;
&lt;p&gt;All four of these APIs are Baseline &lt;em&gt;Newly&lt;/em&gt; available, not Widely available. That distinction matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Newly available:&lt;/strong&gt; All core browsers (Chrome, Edge, Firefox, and Safari across desktop and mobile) support the feature. You can use it in production.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Widely available:&lt;/strong&gt; The feature has been interoperable for 30 months. You can use it without thinking about browser support.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These four are firmly in the &quot;use it today&quot; category. All core browsers ship them. If you still support older Safari or Firefox versions, feature detection is straightforward: &lt;code&gt;&quot;navigation&quot; in window&lt;/code&gt;, &lt;code&gt;CSS.supports(&quot;selector(:open)&quot;)&lt;/code&gt;, &lt;code&gt;@supports (container-name: x)&lt;/code&gt; in CSS. None of these require a polyfill to degrade gracefully; they just need a fallback path.&lt;/p&gt;
&lt;p&gt;The 30-month clock to Widely availability means these become &quot;don't even think about it&quot; territory between mid-2028 and late 2028.&lt;/p&gt;
&lt;h3 id=&quot;the-web-platform-is-eating-libraries-from-the-edges-in&quot;&gt;The web platform is eating libraries from the edges in&lt;/h3&gt;
&lt;p&gt;None of these four APIs kills a major framework. You will still reach for React Router if your app has nested layouts, data loaders, and route-level error boundaries. You will still use a CSS-in-JS library if your design system demands it. What they kill is the dependency you pulled in for &lt;em&gt;one&lt;/em&gt; thing: the router for a content site, the classnames utility for variant switching, the state tracking for modal-open classes.&lt;/p&gt;
&lt;p&gt;The gap between &quot;what the platform gives you&quot; and &quot;what you need a library for&quot; keeps narrowing. &lt;a rel=&quot;nofollow&quot; href=&quot;https://morello.dev/blog/the-new-http-query-method&quot;&gt;The HTTP QUERY method&lt;/a&gt; landed earlier this year for safe reads with a body. Now we have routing, variant-driven styling, state selectors, and precise math, all Baseline, all in every browser.&lt;/p&gt;
&lt;p&gt;We are not at zero dependencies. But each Baseline release shrinks the surface area that justifies pulling in someone else's code. And 2026 has been a good year for that.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23275/baseline-2026-4-apis-that-changed-how-i-code</guid>
<pubDate>Wed, 22 Jul 2026 17:54:25 +0000</pubDate>
</item>
<item>
<title>Tailwind CSS for Beginners: From Zero to Your First Component</title>
<link>https://coderlegion.com/22619/tailwind-css-for-beginners-from-zero-to-your-first-component</link>
<description>&lt;h2 id=&quot;introduction-2&quot;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The first time I saw a Tailwind CSS class list — something like flex items-center justify-between p-4 bg-slate-800 rounded-lg shadow-md — I genuinely thought it looked messy. Why not just write regular CSS?&lt;/p&gt;
&lt;p&gt;Then I actually used it on a real project, and within a week I understood why so many developers (myself included now) switched permanently. You stop context-switching between HTML and a separate CSS file, you stop naming classes like .card-wrapper-inner-2, and you build UI noticeably faster.&lt;/p&gt;
&lt;p&gt;This is the guide I wish I had when I started.&lt;/p&gt;
&lt;h2 id=&quot;what-tailwind-css-actually-is&quot;&gt;What Tailwind CSS Actually Is&lt;/h2&gt;
&lt;p&gt;Tailwind is a utility-first CSS framework. Instead of writing custom CSS classes, you compose your design directly in your HTML using small, single-purpose utility classes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Regular CSS:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.button {
  padding: 8px 16px;
  background-color: #3b82f6;
  border-radius: 8px;
  color: white;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Tailwind:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;button class=&quot;px-4 py-2 bg-blue-500 rounded-lg text-white&quot;&amp;gt;
  Click Me
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same result. No separate CSS file needed for this button.&lt;/p&gt;
&lt;h3 id=&quot;setting-it-up-react-vite-&quot;&gt;Setting It Up (React + Vite)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm create vite@latest my-app -- --template react
cd my-app
npm install tailwindcss @tailwindcss/vite
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;In vite.config.js:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import tailwindcss from '@tailwindcss/vite'

export default {
  plugins: [tailwindcss()],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;In your main CSS file:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@import &quot;tailwindcss&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's it — no separate config file required for modern Tailwind setups. Restart your dev server and you're ready to use utility classes anywhere.&lt;/p&gt;
&lt;h2 id=&quot;core-concepts-you-need-to-know&quot;&gt;Core Concepts You Need to Know&lt;/h2&gt;
&lt;h3 id=&quot;1-spacing&quot;&gt;1. Spacing&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;p-4&lt;/strong&gt; = padding on all sides, &lt;strong&gt;px-4&lt;/strong&gt; = horizontal padding only, &lt;strong&gt;py-4&lt;/strong&gt; = vertical only, &lt;strong&gt;m-4&lt;/strong&gt; = margin. The number is a spacing scale &lt;strong&gt;(4 = 1rem = 16px)&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;2-flexbox-amp-grid-utilities&quot;&gt;2. Flexbox &amp;amp; Grid utilities&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;flex, items-center, justify-between, grid, grid-cols-3, gap-4&lt;/strong&gt; — everything from the Flexbox/Grid article, just as classes.&lt;/p&gt;
&lt;h3 id=&quot;3-responsive-design&quot;&gt;3. Responsive Design&lt;/h3&gt;
&lt;p&gt;Prefix any class with a breakpoint: &lt;strong&gt;md:flex-row&lt;/strong&gt; means &quot;apply this on medium screens and up.&quot;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;flex flex-col md:flex-row gap-4&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This stacks items vertically on mobile, then switches to a row on tablets and larger.&lt;/p&gt;
&lt;h3 id=&quot;4-hover-amp-state-variants&quot;&gt;4. Hover &amp;amp; State Variants&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;button class=&quot;bg-blue-500 hover:bg-blue-600 transition&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;hover:&lt;/strong&gt; applies only on hover. Same pattern works for &lt;strong&gt;focus:, active:, disabled:&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&quot;building-your-first-component-a-pricing-card&quot;&gt;Building Your First Component: A Pricing Card&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;max-w-sm mx-auto bg-slate-800 rounded-2xl p-6 shadow-lg&quot;&amp;gt;
  &amp;lt;h3 class=&quot;text-xl font-bold text-white mb-2&quot;&amp;gt;Starter Plan&amp;lt;/h3&amp;gt;
  &amp;lt;p class=&quot;text-slate-400 mb-4&quot;&amp;gt;Perfect for small projects&amp;lt;/p&amp;gt;
  &amp;lt;p class=&quot;text-3xl font-bold text-white mb-6&quot;&amp;gt;$9&amp;lt;span class=&quot;text-base font-normal text-slate-400&quot;&amp;gt;/mo&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
  &amp;lt;button class=&quot;w-full py-2 bg-blue-500 hover:bg-blue-600 rounded-lg text-white font-medium transition&quot;&amp;gt;
    Get Started
  &amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice what's happening: &lt;strong&gt;max-w-sm mx-auto&lt;/strong&gt; centers and constrains the card width, &lt;strong&gt;rounded-2xl&lt;/strong&gt; and &lt;strong&gt;shadow-lg&lt;/strong&gt; give it depth, and everything is readable directly from the class names — no jumping to a separate file to understand the styling.&lt;/p&gt;
&lt;h2 id=&quot;common-beginner-mistakes-1&quot;&gt;Common Beginner Mistakes&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Fighting the spacing scale. Don't reach for custom pixel values&lt;br&gt;
immediately — Tailwind's default scale (4, 8, 12, 16...) covers 95%&lt;br&gt;
of real use cases.&lt;/li&gt;
&lt;li&gt;Not using @apply for repeated patterns. If you're copy-pasting the&lt;br&gt;
same 8 classes everywhere, extract them into a component instead of&lt;br&gt;
a CSS class — that's the React way to do it, not @apply.&lt;/li&gt;
&lt;li&gt;Ignoring the official docs. Tailwind's docs (tailwindcss.com) are&lt;br&gt;
genuinely excellent — searchable, example-driven, and the fastest&lt;br&gt;
way to find the exact class you need.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;final-thoughts-3&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Tailwind feels weird for about a week, then it feels like the only sane way to write CSS. The speed difference is real — I build UI noticeably faster now than when I wrote custom CSS files for every component.&lt;/p&gt;
&lt;p&gt;Start small: rebuild one existing component using Tailwind instead of your old CSS, and see how it feels.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Muhammad Farhan&lt;/strong&gt; is a Frontend Developer specializing in React.js and Tailwind CSS, based in Dera Ismail Khan, Pakistan.&lt;br&gt;
Portfolio: muhammad-farhan-dev.github.io/muhammadfarhan.dev&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22619/tailwind-css-for-beginners-from-zero-to-your-first-component</guid>
<pubDate>Tue, 21 Jul 2026 05:00:00 +0000</pubDate>
</item>
<item>
<title>shadcn/typeset vs Tailwind Typography for Markdown</title>
<link>https://coderlegion.com/22961/shadcn-typeset-vs-tailwind-typography-for-markdown</link>
<description>&lt;p&gt;You render some markdown, get back a pile of unstyled HTML, and then you style it. Headings, paragraphs, lists, tables, code. You do it for the blog. Then you do it again for the docs. Then a third time for the chat UI that streams model output token by token. Every context, the same elements, slightly different rules.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/shadcn&quot;&gt;shadcn&lt;/a&gt; shipped a take on this on July 10, 2026, called &lt;a rel=&quot;nofollow&quot; href=&quot;https://ui.shadcn.com/docs/typeset&quot;&gt;shadcn/typeset&lt;/a&gt;. It's worth a look even if you don't use the rest of shadcn/ui, because it isn't a component and it isn't a CLI install. It's one CSS file you copy into your project and own outright.&lt;/p&gt;
&lt;h3 id=&quot;what-is-shadcn-typeset-&quot;&gt;What is shadcn/typeset?&lt;/h3&gt;
&lt;p&gt;Typeset is a styling system for rendered HTML and markdown, delivered as a single CSS file. You wrap your content in a &lt;code&gt;.typeset&lt;/code&gt; container and everything inside it gets styled: headings, paragraphs, lists, tables, code, blockquotes, links, footnotes, even MathML and &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;&amp;lt;div className=&quot;typeset typeset-docs&quot;&amp;gt;
  &amp;lt;YourMarkdownRenderer&amp;gt;{content}&amp;lt;/YourMarkdownRenderer&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.typeset&lt;/code&gt; turns the styles on. &lt;code&gt;.typeset-docs&lt;/code&gt; is a preset, a small class you layer on top to tune the look for a specific context. You can define as many presets as you have contexts: one for the blog, one for the docs, one for chat.&lt;/p&gt;
&lt;p&gt;The thing to internalize up front: there's no npm package and no config layer. As the &lt;a rel=&quot;nofollow&quot; href=&quot;https://ui.shadcn.com/docs/changelog/2026-07-typeset&quot;&gt;changelog&lt;/a&gt; puts it, &quot;the file lives in your project,&quot; nothing to update, nothing to work around. If a heading annoys you, you open the file and change the rule. That's the whole distribution model.&lt;/p&gt;
&lt;p&gt;This is a different thing from shadcn/ui's &lt;a rel=&quot;nofollow&quot; href=&quot;https://ui.shadcn.com/docs/components/base/typography&quot;&gt;Typography component&lt;/a&gt;, which gives you styled React heading and paragraph components to author by hand. Typeset styles HTML you didn't write, the output of a markdown renderer.&lt;/p&gt;
&lt;h3 id=&quot;how-do-you-install-shadcn-typeset-&quot;&gt;How do you install shadcn/typeset?&lt;/h3&gt;
&lt;p&gt;You don't run a CLI. As of this release there's no &lt;code&gt;npx shadcn@latest add typeset&lt;/code&gt;, and expecting one will send you looking for a command that doesn't exist.&lt;/p&gt;
&lt;p&gt;Instead you build the file in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://ui.shadcn.com/typeset&quot;&gt;interactive builder&lt;/a&gt;, pick your sizes and fonts, and copy the generated &lt;code&gt;typeset.css&lt;/code&gt; into your project. Then you import it after Tailwind:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@import &quot;tailwindcss&quot;;
@import &quot;./typeset.css&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Import order matters, and I'll come back to why in a moment. Once it's in, the wrapper from the first snippet is all you need.&lt;/p&gt;
&lt;h3 id=&quot;size-leading-and-flow&quot;&gt;Size, leading, and flow&lt;/h3&gt;
&lt;p&gt;Typeset condenses typographic tuning into three CSS variables it calls &quot;rhythm,&quot; and everything else derives from them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.typeset {
  --typeset-size: 1em;      /* base font size */
  --typeset-leading: 1.75;  /* line-height */
  --typeset-flow: 1.25em;   /* vertical space between blocks */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A preset is just those three variables set to different values under a class name:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.typeset-chat {
  --typeset-flow: 1em;
  --typeset-leading: 1.6;
}
.typeset-docs {
  --typeset-size: 15px;
  --typeset-flow: 1.5em;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because it's all CSS custom properties, you can override a single value inline without touching the file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;&amp;lt;article className=&quot;typeset [--typeset-flow:1.75em]&quot;&amp;gt;{content}&amp;lt;/article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The sizing is container-relative rather than a fixed &lt;code&gt;rem&lt;/code&gt; scale. Below &lt;code&gt;48rem&lt;/code&gt; the base size gets a &lt;code&gt;1.125x&lt;/code&gt; bump and then settles back to the raw value on larger screens, so text reads comfortably on a phone without a separate small-screen variant. Colors and borders pull from your app's theme tokens (&lt;code&gt;--color-foreground&lt;/code&gt;, &lt;code&gt;--color-muted-foreground&lt;/code&gt;, &lt;code&gt;--color-border&lt;/code&gt;, &lt;code&gt;--radius&lt;/code&gt;) with sensible fallbacks, so dark mode just works when your tokens flip. There's no second inverted palette to maintain.&lt;/p&gt;
&lt;p&gt;One deliberate omission: Typeset doesn't set a &lt;code&gt;max-width&lt;/code&gt;. Your layout owns the measure. The builder's Measure control adds a &lt;code&gt;max-width&lt;/code&gt; to the wrapper if you want it, but the stylesheet itself stays out of your layout's business.&lt;/p&gt;
&lt;h3 id=&quot;why-your-tailwind-utilities-win-without-important&quot;&gt;Why your Tailwind utilities win without !important&lt;/h3&gt;
&lt;p&gt;Here's the detail I liked most. Typeset lives in the CSS &lt;code&gt;@components&lt;/code&gt; layer and uses zero-specificity &lt;code&gt;:where()&lt;/code&gt; selectors for every element rule:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@layer components {
  .typeset {
    &amp;amp; :where(h1) { font-size: 1.75em; /* ... */ }
    &amp;amp; :where(p)  { margin-block-start: var(--typeset-flow); }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The override comes from cascade layers, not specificity. Typeset's rules live in the &lt;code&gt;@components&lt;/code&gt; layer, and Tailwind's utilities live in the &lt;code&gt;@utilities&lt;/code&gt; layer, which is declared after it. When rules in two different layers conflict, the later layer wins outright, no matter how specific either selector is. So a utility beats a Typeset rule with no &lt;code&gt;!important&lt;/code&gt; and no special modifier syntax. Slap &lt;code&gt;.text-3xl&lt;/code&gt; on a heading and it just wins.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;:where()&lt;/code&gt; does a related but separate job: it holds every element rule at zero specificity, so an override that isn't already shielded by a later layer, like plain CSS you write yourself or a rule in the same layer, can still win without a specificity fight. That's also why import order matters. Importing Tailwind first establishes its layer order, which puts &lt;code&gt;@utilities&lt;/code&gt; after Typeset's &lt;code&gt;@components&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you want to pull a whole subtree out, there's an escape hatch: &lt;code&gt;.not-typeset&lt;/code&gt; (or the &lt;code&gt;data-not-typeset&lt;/code&gt; attribute), which cascades to descendants including any nested &lt;code&gt;.typeset&lt;/code&gt; container.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;&amp;lt;Card className=&quot;not-typeset&quot;&amp;gt;Untouched component.&amp;lt;/Card&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;shadcn is candid that both of these ideas, the &lt;code&gt;:where()&lt;/code&gt; guard and the opt-out class, are borrowed from &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/tailwindlabs/tailwindcss-typography&quot;&gt;&lt;code&gt;@tailwindcss/typography&lt;/code&gt;&lt;/a&gt;, which has a &lt;code&gt;.not-prose&lt;/code&gt; of its own. Which brings us to the comparison you actually clicked for.&lt;/p&gt;
&lt;h3 id=&quot;shadcn-typeset-vs-tailwind-typography&quot;&gt;shadcn/typeset vs Tailwind Typography&lt;/h3&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/tailwindlabs/tailwindcss-typography&quot;&gt;Tailwind CSS Typography&lt;/a&gt; plugin, and the &lt;code&gt;.prose&lt;/code&gt; class it hands you, has been the default answer for styling rendered markdown for years, and it's still excellent at what it was built for. Typeset isn't a drop-in replacement or a wrapper around it. It's a different set of trade-offs. The docs lay them out directly:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Tailwind Typography (&lt;code&gt;.prose&lt;/code&gt;) &lt;/th&gt;&lt;th&gt;Typeset&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Sizing &lt;/td&gt;&lt;td&gt;Fixed &lt;code&gt;rem&lt;/code&gt; scale, &lt;code&gt;.prose-sm&lt;/code&gt; to &lt;code&gt;.prose-2xl&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Relative to the container, any size&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Dark mode &lt;/td&gt;&lt;td&gt;&lt;code&gt;.prose-invert&lt;/code&gt;, a second palette &lt;/td&gt;&lt;td&gt;Your tokens flip, nothing to add&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Overrides &lt;/td&gt;&lt;td&gt;&lt;code&gt;prose-a:&lt;/code&gt;, &lt;code&gt;prose-headings:&lt;/code&gt; modifier API &lt;/td&gt;&lt;td&gt;Plain utilities and CSS win&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Streaming &lt;/td&gt;&lt;td&gt;No append-stability contract &lt;/td&gt;&lt;td&gt;Designed for stable appends&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Distribution &lt;/td&gt;&lt;td&gt;npm plugin, generated CSS &lt;/td&gt;&lt;td&gt;One CSS file you own&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The way I'd frame it: &lt;code&gt;.prose&lt;/code&gt; is a well-designed dependency you configure, and Typeset is a starting point you edit. If you like the modifier API and want a maintained plugin tracking Tailwind releases, &lt;code&gt;.prose&lt;/code&gt; is still a great pick. If you'd rather own the file and delete the rules you don't want, Typeset fits that instinct better. It's the same philosophy as the rest of shadcn/ui: copy the code in, make it yours.&lt;/p&gt;
&lt;h3 id=&quot;what-makes-it-streaming-safe-&quot;&gt;What makes it streaming-safe?&lt;/h3&gt;
&lt;p&gt;This is the part that made Typeset feel designed for 2026 rather than 2020. If you've built a chat UI that renders an LLM's markdown as it streams, you've watched the layout twitch as each token lands. Typeset treats that as a first-class constraint.&lt;/p&gt;
&lt;p&gt;The rule is that there are no forward-looking selectors in the layout. &lt;code&gt;:last-child&lt;/code&gt;, &lt;code&gt;:has()&lt;/code&gt;, and &lt;code&gt;:empty&lt;/code&gt; are deliberately kept out of spacing rules, because what they match changes as content appends, which restyles blocks that were already on screen. Spacing flows in one direction only, using &lt;code&gt;margin-block-start&lt;/code&gt; and never &lt;code&gt;margin-block-end&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;&amp;amp; :where(p) {
  margin-block-start: var(--typeset-flow);
  margin-block-end: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same discipline shows up in tables: the row separators sit on the cells rather than on &lt;code&gt;tr:last-child&lt;/code&gt;, so appending a row never re-runs a &lt;code&gt;:last-child&lt;/code&gt; match and shifts the borders above it. When you add a new block to a Typeset container, nothing before it moves. That's a genuinely hard property to get right by hand, and it's baked into the stylesheet.&lt;/p&gt;
&lt;h3 id=&quot;what-it-costs-you&quot;&gt;What it costs you&lt;/h3&gt;
&lt;p&gt;Two things are worth knowing before you commit.&lt;/p&gt;
&lt;p&gt;First, the stylesheet leans on modern CSS, though most of it is safe by now: &lt;code&gt;color-mix()&lt;/code&gt; and &lt;code&gt;oklch()&lt;/code&gt; colors have been widely supported since 2023, and logical properties like &lt;code&gt;margin-block-start&lt;/code&gt; work everywhere. The real outlier is &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/margin-trim&quot;&gt;&lt;code&gt;margin-trim&lt;/code&gt;&lt;/a&gt;, which as of 2026 still ships only in Safari and other WebKit browsers (16.4 and up). There's no fallback for it, so in Chrome and Firefox that rule quietly does nothing. Worth knowing before you count on the margin trimming it's meant to handle.&lt;/p&gt;
&lt;p&gt;Second, wide tables wrap to fit by default. Horizontal scroll is opt-in through a &lt;code&gt;.typeset-scroll&lt;/code&gt; wrapper you have to add in your renderer's table component or a small rehype plugin. It won't happen automatically.&lt;/p&gt;
&lt;h3 id=&quot;should-you-use-it-&quot;&gt;Should you use it?&lt;/h3&gt;
&lt;p&gt;If you're already in the shadcn/ui world and you're styling rendered markdown, especially anything that streams, Typeset is an easy yes: it's free, it's one file, and the streaming stability alone justifies it. If you're outside that ecosystem and happy with &lt;code&gt;.prose&lt;/code&gt;, there's no migration to rush. It's an alternative with a clearer answer to a couple of specific problems, not an upgrade you're missing out on.&lt;/p&gt;
&lt;p&gt;What I appreciate is the framing. shadcn keeps betting that the right unit of distribution for UI is source you own rather than a dependency you configure, and Typeset applies that bet to typography. I made a similar call &lt;a rel=&quot;nofollow&quot; href=&quot;https://morello.dev/blog/the-new-website&quot;&gt;rebuilding this site&lt;/a&gt;: the long-form styling here is hand-written Tailwind wired to theme variables rather than a plugin, for exactly the reason Typeset exists. The same instinct runs through &lt;a rel=&quot;nofollow&quot; href=&quot;https://morello.dev/blog/generating-og-images-with-satori-and-astro&quot;&gt;generating OG images with Satori&lt;/a&gt;: own the design, skip the abstraction. Owning the CSS means when something looks off, you fix the rule instead of fighting the abstraction. Go &lt;a rel=&quot;nofollow&quot; href=&quot;https://ui.shadcn.com/typeset&quot;&gt;build one in the builder&lt;/a&gt; and read the file it hands you. It's short, and reading it teaches you more about typographic CSS than the docs do.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22961/shadcn-typeset-vs-tailwind-typography-for-markdown</guid>
<pubDate>Sat, 18 Jul 2026 16:44:34 +0000</pubDate>
</item>
<item>
<title>CSS Flexbox vs Grid: When to Use Which (Beginner's Guide)</title>
<link>https://coderlegion.com/22607/css-flexbox-vs-grid-when-to-use-which-beginners-guide</link>
<description>&lt;h2 id=&quot;introduction-3&quot;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Every beginner hits this wall eventually: &quot;Do I use Flexbox or Grid here?&quot;&lt;/p&gt;
&lt;p&gt;I remember avoiding this question for months by just using Flexbox for everything — including things Grid was clearly built for. It worked, technically, but I was fighting the tool instead of using the right one.&lt;/p&gt;
&lt;p&gt;Here's the short version before we go deep: Flexbox is for one direction (a row or a column). Grid is for two directions at once (rows AND columns together). Once that clicks, the decision gets easy.&lt;/p&gt;
&lt;p&gt;Let's break it down properly.&lt;/p&gt;
&lt;h2 id=&quot;what-flexbox-actually-does&quot;&gt;What Flexbox Actually Does&lt;/h2&gt;
&lt;p&gt;Flexbox arranges items along a single axis — either horizontally or vertically.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.container {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;justify-content controls spacing along the main axis (usually horizontal), and align-items controls alignment on the cross axis (usually vertical).&lt;/p&gt;
&lt;h3 id=&quot;flexbox-is-perfect-for-&quot;&gt;Flexbox is perfect for:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Navigation bars (logo left, links right)&lt;/li&gt;
&lt;li&gt;Buttons sitting side by side&lt;/li&gt;
&lt;li&gt;Centering a single element vertically and horizontally&lt;/li&gt;
&lt;li&gt;Card content where items need to stretch or wrap in one direction&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-grid-actually-does&quot;&gt;What Grid Actually Does&lt;/h2&gt;
&lt;p&gt;Grid lets you define rows and columns at the same time, and place items precisely into that structure.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.container {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates 3 equal-width columns with 20px gaps — no media queries needed to get a basic responsive layout going.&lt;/p&gt;
&lt;h3 id=&quot;grid-is-perfect-for-&quot;&gt;Grid is perfect for:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Full page layouts (header, sidebar, main content, footer)&lt;/li&gt;
&lt;li&gt;Image galleries&lt;/li&gt;
&lt;li&gt;Card grids (like a blog listing page)&lt;/li&gt;
&lt;li&gt;Anything where you're thinking in rows AND columns&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Real Test: Ask Yourself This&lt;/p&gt;
&lt;p&gt;&quot;Am I arranging things in one line, or am I building a layout?&quot;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One line → Flexbox&lt;/li&gt;
&lt;li&gt;Layout/grid of items → Grid&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;a-practical-example-building-a-card-grid&quot;&gt;A Practical Example: Building a Card Grid&lt;/h3&gt;
&lt;p&gt;Here's a common beginner project — a responsive grid of cards, done the right way with Grid:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;grid grid-cols-1 md:grid-cols-3 gap-6&quot;&amp;gt;
  &amp;lt;div class=&quot;bg-slate-800 p-4 rounded-lg&quot;&amp;gt;Card 1&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;bg-slate-800 p-4 rounded-lg&quot;&amp;gt;Card 2&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;bg-slate-800 p-4 rounded-lg&quot;&amp;gt;Card 3&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(That's Tailwind CSS classes, but it maps directly to Grid under the hood — grid, grid-cols-3, gap-6.)&lt;/p&gt;
&lt;p&gt;Now, inside each card, if you need to align an icon next to a title, or push a button to the bottom of the card — that's Flexbox's job:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;bg-slate-800 p-4 rounded-lg flex flex-col justify-between&quot;&amp;gt;
  &amp;lt;div class=&quot;flex items-center gap-2&quot;&amp;gt;
    &amp;lt;span&amp;gt;🔥&amp;lt;/span&amp;gt;
    &amp;lt;h3&amp;gt;Card Title&amp;lt;/h3&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;button&amp;gt;Learn More&amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the pattern you'll use constantly: Grid for the outer layout, Flexbox for the inner content.&lt;/p&gt;
&lt;h2 id=&quot;common-beginner-mistakes-2&quot;&gt;Common Beginner Mistakes&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Using Grid for a simple navbar. Overkill — Flexbox does this in 3&lt;br&gt;
lines.&lt;/li&gt;
&lt;li&gt;Using Flexbox for a full page layout. You'll end up wrapping and&lt;br&gt;
fighting widths that Grid handles natively.&lt;/li&gt;
&lt;li&gt;Not using gap. Both Flexbox and Grid support gap now — no more&lt;br&gt;
margin hacks between items.&lt;/li&gt;
&lt;li&gt;Forgetting Grid can do more than equal columns.&lt;br&gt;
grid-template-columns: 200px 1fr gives you a fixed sidebar +&lt;br&gt;
flexible main content in one line.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;final-thoughts-4&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;You don't have to pick one and stick with it forever — most real websites use both together, Grid for structure and Flexbox for alignment inside components. Once you stop treating this as an either/or decision and start asking &quot;one direction or two?&quot;, it stops being confusing.&lt;/p&gt;
&lt;p&gt;Try rebuilding one of your old projects using this rule and see how much cleaner the CSS gets.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Muhammad Farhan&lt;/strong&gt; is a &lt;strong&gt;Frontend Developer&lt;/strong&gt; specializing in React.js and Tailwind CSS, based in &lt;strong&gt;Dera Ismail Khan, Pakistan&lt;/strong&gt;.&lt;br&gt;
Portfolio: &lt;a rel=&quot;nofollow&quot; href=&quot;https://muhammad-farhan-dev.github.io/muhammadfarhan.dev&quot;&gt;Muhammad Farhan&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22607/css-flexbox-vs-grid-when-to-use-which-beginners-guide</guid>
<pubDate>Fri, 17 Jul 2026 05:00:00 +0000</pubDate>
</item>
<item>
<title>shadcn/typeset vs Tailwind Typography: Styling Markdown</title>
<link>https://coderlegion.com/22285/shadcn-typeset-vs-tailwind-typography-styling-markdown</link>
<description>&lt;p&gt;You render some markdown, get back a pile of unstyled HTML, and then you style it. Headings, paragraphs, lists, tables, code. You do it for the blog. Then you do it again for the docs. Then a third time for the chat UI that streams model output token by token. Every context, the same elements, slightly different rules.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/shadcn&quot;&gt;shadcn&lt;/a&gt; shipped a take on this on July 10, 2026, called &lt;a rel=&quot;nofollow&quot; href=&quot;https://ui.shadcn.com/docs/typeset&quot;&gt;shadcn/typeset&lt;/a&gt;. It's worth a look even if you don't use the rest of shadcn/ui, because it isn't a component and it isn't a CLI install. It's one CSS file you copy into your project and own outright.&lt;/p&gt;
&lt;h3 id=&quot;what-is-shadcn-typeset--1&quot;&gt;What is shadcn/typeset?&lt;/h3&gt;
&lt;p&gt;Typeset is a styling system for rendered HTML and markdown, delivered as a single CSS file. You wrap your content in a &lt;code&gt;.typeset&lt;/code&gt; container and everything inside it gets styled: headings, paragraphs, lists, tables, code, blockquotes, links, footnotes, even MathML and &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;&amp;lt;div className=&quot;typeset typeset-docs&quot;&amp;gt;
  &amp;lt;YourMarkdownRenderer&amp;gt;{content}&amp;lt;/YourMarkdownRenderer&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.typeset&lt;/code&gt; turns the styles on. &lt;code&gt;.typeset-docs&lt;/code&gt; is a preset, a small class you layer on top to tune the look for a specific context. You can define as many presets as you have contexts: one for the blog, one for the docs, one for chat.&lt;/p&gt;
&lt;p&gt;The thing to internalize up front: there's no npm package and no config layer. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://ui.shadcn.com/docs/changelog/2026-07-typeset&quot;&gt;changelog&lt;/a&gt; puts it plainly: &quot;one CSS file that styles everything inside a &lt;code&gt;typeset&lt;/code&gt; container. The file lives in your project, so you can change it directly when you need to.&quot; If a heading annoys you, you open the file and change the rule. That's the whole distribution model.&lt;/p&gt;
&lt;p&gt;This is a different thing from shadcn/ui's &lt;a rel=&quot;nofollow&quot; href=&quot;https://ui.shadcn.com/docs/components/typography&quot;&gt;Typography component&lt;/a&gt;, which gives you styled React heading and paragraph components to author by hand. Typeset styles HTML you didn't write, the output of a markdown renderer.&lt;/p&gt;
&lt;h3 id=&quot;how-do-you-install-shadcn-typeset--1&quot;&gt;How do you install shadcn/typeset?&lt;/h3&gt;
&lt;p&gt;You don't run a CLI. As of this release there's no &lt;code&gt;npx shadcn@latest add typeset&lt;/code&gt;, and expecting one will send you looking for a command that doesn't exist.&lt;/p&gt;
&lt;p&gt;Instead you build the file in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://ui.shadcn.com/typeset&quot;&gt;interactive builder&lt;/a&gt;, pick your sizes and fonts, and copy the generated &lt;code&gt;typeset.css&lt;/code&gt; into your project. Then you import it after Tailwind:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@import &quot;tailwindcss&quot;;
@import &quot;./typeset.css&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Import order matters, and I'll come back to why in a moment. Once it's in, the wrapper from the first snippet is all you need.&lt;/p&gt;
&lt;h3 id=&quot;size-leading-and-flow-1&quot;&gt;Size, leading, and flow&lt;/h3&gt;
&lt;p&gt;Typeset condenses typographic tuning into three CSS variables it calls &quot;rhythm,&quot; and everything else derives from them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.typeset {
  --typeset-size: 1em;      /* base font size */
  --typeset-leading: 1.75;  /* line-height */
  --typeset-flow: 1.25em;   /* vertical space between blocks */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A preset is just those three variables set to different values under a class name:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.typeset-chat {
  --typeset-flow: 1em;
  --typeset-leading: 1.6;
}
.typeset-docs {
  --typeset-size: 15px;
  --typeset-flow: 1.5em;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because it's all CSS custom properties, you can override a single value inline without touching the file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;&amp;lt;article className=&quot;typeset [--typeset-flow:1.75em]&quot;&amp;gt;{content}&amp;lt;/article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The sizing is container-relative rather than a fixed &lt;code&gt;rem&lt;/code&gt; scale. Below &lt;code&gt;48rem&lt;/code&gt; the base size gets a &lt;code&gt;1.125x&lt;/code&gt; bump and then settles back to the raw value on larger screens, so text reads comfortably on a phone without a separate small-screen variant. Colors and borders pull from your app's theme tokens (&lt;code&gt;--color-foreground&lt;/code&gt;, &lt;code&gt;--color-muted-foreground&lt;/code&gt;, &lt;code&gt;--color-border&lt;/code&gt;, &lt;code&gt;--radius&lt;/code&gt;) with sensible fallbacks, so dark mode just works when your tokens flip. There's no second inverted palette to maintain.&lt;/p&gt;
&lt;p&gt;One deliberate omission: Typeset doesn't set a &lt;code&gt;max-width&lt;/code&gt;. Your layout owns the measure. The builder's Measure control adds a &lt;code&gt;max-width&lt;/code&gt; to the wrapper if you want it, but the stylesheet itself stays out of your layout's business.&lt;/p&gt;
&lt;h3 id=&quot;why-your-tailwind-utilities-win-without-important-1&quot;&gt;Why your Tailwind utilities win without !important&lt;/h3&gt;
&lt;p&gt;Here's the detail I liked most. Typeset lives in the CSS &lt;code&gt;@components&lt;/code&gt; layer and uses zero-specificity &lt;code&gt;:where()&lt;/code&gt; selectors for every element rule:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@layer components {
  .typeset {
    &amp;amp; :where(h1) { font-size: 1.75em; /* ... */ }
    &amp;amp; :where(p)  { margin-block-start: var(--typeset-flow); }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The override comes from cascade layers, not specificity. Typeset's rules live in the &lt;code&gt;@components&lt;/code&gt; layer, and Tailwind's utilities live in the &lt;code&gt;@utilities&lt;/code&gt; layer, which is declared after it. When rules in two different layers conflict, the later layer wins outright, no matter how specific either selector is. So a utility beats a Typeset rule with no &lt;code&gt;!important&lt;/code&gt; and no special modifier syntax. Slap &lt;code&gt;.text-3xl&lt;/code&gt; on a heading and it just wins.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;:where()&lt;/code&gt; does a related but separate job: it holds every element rule at zero specificity, so an override that isn't already shielded by a later layer, like plain CSS you write yourself or a rule in the same layer, can still win without a specificity fight. That's also why import order matters. Importing Tailwind first establishes its layer order, which puts &lt;code&gt;@utilities&lt;/code&gt; after Typeset's &lt;code&gt;@components&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you want to pull a whole subtree out, there's an escape hatch: &lt;code&gt;.not-typeset&lt;/code&gt; (or the &lt;code&gt;data-not-typeset&lt;/code&gt; attribute), which cascades to descendants including any nested &lt;code&gt;.typeset&lt;/code&gt; container.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;&amp;lt;Card className=&quot;not-typeset&quot;&amp;gt;Untouched component.&amp;lt;/Card&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;shadcn is candid that both of these ideas, the &lt;code&gt;:where()&lt;/code&gt; guard and the opt-out class, are borrowed from &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/tailwindlabs/tailwindcss-typography&quot;&gt;&lt;code&gt;@tailwindcss/typography&lt;/code&gt;&lt;/a&gt;, which has a &lt;code&gt;.not-prose&lt;/code&gt; of its own. Which brings us to the comparison you actually clicked for.&lt;/p&gt;
&lt;h3 id=&quot;shadcn-typeset-vs-tailwind-typography-1&quot;&gt;shadcn/typeset vs Tailwind Typography&lt;/h3&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/tailwindlabs/tailwindcss-typography&quot;&gt;Tailwind CSS Typography&lt;/a&gt; plugin, and the &lt;code&gt;.prose&lt;/code&gt; class it hands you, has been the default answer for styling rendered markdown for years, and it's still excellent at what it was built for. Typeset isn't a drop-in replacement or a wrapper around it. It's a different set of trade-offs. The docs lay them out directly:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Tailwind Typography (&lt;code&gt;.prose&lt;/code&gt;) &lt;/th&gt;&lt;th&gt;Typeset&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Sizing &lt;/td&gt;&lt;td&gt;Fixed &lt;code&gt;rem&lt;/code&gt; scale, &lt;code&gt;.prose-sm&lt;/code&gt; to &lt;code&gt;.prose-2xl&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Relative to the container, any size&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Dark mode &lt;/td&gt;&lt;td&gt;&lt;code&gt;.prose-invert&lt;/code&gt;, a second palette &lt;/td&gt;&lt;td&gt;Your tokens flip, nothing to add&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Overrides &lt;/td&gt;&lt;td&gt;&lt;code&gt;prose-a:&lt;/code&gt;, &lt;code&gt;prose-headings:&lt;/code&gt; modifier API &lt;/td&gt;&lt;td&gt;Plain utilities and CSS win&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Streaming &lt;/td&gt;&lt;td&gt;No append-stability contract &lt;/td&gt;&lt;td&gt;Designed for stable appends&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Distribution &lt;/td&gt;&lt;td&gt;npm plugin, generated CSS &lt;/td&gt;&lt;td&gt;One CSS file you own&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The way I'd frame it: &lt;code&gt;.prose&lt;/code&gt; is a well-designed dependency you configure, and Typeset is a starting point you edit. If you like the modifier API and want a maintained plugin tracking Tailwind releases, &lt;code&gt;.prose&lt;/code&gt; is still a great pick. If you'd rather own the file and delete the rules you don't want, Typeset fits that instinct better. It's the same philosophy as the rest of shadcn/ui: copy the code in, make it yours.&lt;/p&gt;
&lt;h3 id=&quot;what-makes-it-streaming-safe--1&quot;&gt;What makes it streaming-safe?&lt;/h3&gt;
&lt;p&gt;This is the part that made Typeset feel designed for 2026 rather than 2020. If you've built a chat UI that renders an LLM's markdown as it streams, you've watched the layout twitch as each token lands. Typeset treats that as a first-class constraint.&lt;/p&gt;
&lt;p&gt;The rule is that there are no forward-looking selectors in the layout. &lt;code&gt;:last-child&lt;/code&gt;, &lt;code&gt;:has()&lt;/code&gt;, and &lt;code&gt;:empty&lt;/code&gt; are deliberately kept out of spacing rules, because what they match changes as content appends, which restyles blocks that were already on screen. Spacing flows in one direction only, using &lt;code&gt;margin-block-start&lt;/code&gt; and never &lt;code&gt;margin-block-end&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;&amp;amp; :where(p) {
  margin-block-start: var(--typeset-flow);
  margin-block-end: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same discipline shows up in tables: the row separators sit on the cells rather than on &lt;code&gt;tr:last-child&lt;/code&gt;, so appending a row never re-runs a &lt;code&gt;:last-child&lt;/code&gt; match and shifts the borders above it. When you add a new block to a Typeset container, nothing before it moves. That's a genuinely hard property to get right by hand, and it's baked into the stylesheet.&lt;/p&gt;
&lt;h3 id=&quot;what-it-costs-you-1&quot;&gt;What it costs you&lt;/h3&gt;
&lt;p&gt;Two things are worth knowing before you commit.&lt;/p&gt;
&lt;p&gt;First, the stylesheet leans on modern CSS, though most of it is safe by now: &lt;code&gt;color-mix()&lt;/code&gt; and &lt;code&gt;oklch()&lt;/code&gt; colors have been widely supported since 2023, and logical properties like &lt;code&gt;margin-block-start&lt;/code&gt; work everywhere. The real outlier is &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/margin-trim&quot;&gt;&lt;code&gt;margin-trim&lt;/code&gt;&lt;/a&gt;, which as of 2026 still ships only in Safari and other WebKit browsers (16.4 and up). There's no fallback for it, so in Chrome and Firefox that rule quietly does nothing. Worth knowing before you count on the margin trimming it's meant to handle.&lt;/p&gt;
&lt;p&gt;Second, wide tables wrap to fit by default. Horizontal scroll is opt-in through a &lt;code&gt;.typeset-scroll&lt;/code&gt; wrapper you have to add in your renderer's table component or a small rehype plugin. It won't happen automatically.&lt;/p&gt;
&lt;h3 id=&quot;should-you-use-it--1&quot;&gt;Should you use it?&lt;/h3&gt;
&lt;p&gt;If you're already in the shadcn/ui world and you're styling rendered markdown, especially anything that streams, Typeset is an easy yes: it's free, it's one file, and the streaming stability alone justifies it. If you're outside that ecosystem and happy with &lt;code&gt;.prose&lt;/code&gt;, there's no migration to rush. It's an alternative with a clearer answer to a couple of specific problems, not an upgrade you're missing out on.&lt;/p&gt;
&lt;p&gt;What I appreciate is the framing. shadcn keeps betting that the right unit of distribution for UI is source you own rather than a dependency you configure, and Typeset applies that bet to typography. I made a similar call &lt;a rel=&quot;nofollow&quot; href=&quot;/blog/the-new-website&quot;&gt;rebuilding this site&lt;/a&gt;: the long-form styling here is hand-written Tailwind wired to theme variables rather than a plugin, for exactly the reason Typeset exists. Owning the CSS means when something looks off, you fix the rule instead of fighting the abstraction. Go &lt;a rel=&quot;nofollow&quot; href=&quot;https://ui.shadcn.com/typeset&quot;&gt;build one in the builder&lt;/a&gt; and read the file it hands you. It's short, and reading it teaches you more about typographic CSS than the docs do.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22285/shadcn-typeset-vs-tailwind-typography-styling-markdown</guid>
<pubDate>Sat, 11 Jul 2026 08:23:11 +0000</pubDate>
</item>
<item>
<title>What Was the First Project You Ever Built in an Online Code Editor?</title>
<link>https://coderlegion.com/21719/what-was-the-first-project-you-ever-built-in-an-online-code-editor</link>
<description>&lt;p&gt;Everyone remembers their first &quot;Aha!&quot; moment in web development. For a lot of us, it didn't happen in a complex local terminal environment; it happened in a browser.&lt;/p&gt;
&lt;p&gt;Maybe you were searching for &quot;how to test HTML CSS JS online&quot; and stumbled into a basic code playground. You changed a background color to &quot;red,&quot; saw the preview update instantly, and realized you could actually build things for the web.&lt;/p&gt;
&lt;p&gt;Online IDEs have always been the gateway to frontend development. Even today, as experienced engineers, we still rely on JavaScript playgrounds to rapidly prototype ideas, test snippets, or share solutions.&lt;/p&gt;
&lt;p&gt;I’ve been building NitroIDE to serve as a modern, distraction-free browser IDE for exactly these moments, but it got me feeling nostalgic.&lt;/p&gt;
&lt;p&gt;What was the very first thing you ever built in a browser-based code editor? Was it a messy personal website, a broken CSS animation, or a calculator app? Let's share some stories.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21719/what-was-the-first-project-you-ever-built-in-an-online-code-editor</guid>
<pubDate>Wed, 01 Jul 2026 15:28:00 +0000</pubDate>
</item>
<item>
<title>CSS Box Shadow: The Complete Guide (with Copy-Paste Examples)</title>
<link>https://coderlegion.com/20695/css-box-shadow-the-complete-guide-with-copy-paste-examples</link>
<description>&lt;p&gt;The &lt;code&gt;box-shadow&lt;/code&gt; property sounds simple — but once you start layering shadows, building card elevation systems, or trying to recreate that exact Figma drop shadow in CSS, the syntax gets complicated fast.&lt;/p&gt;
&lt;p&gt;This guide covers everything from the basics to advanced multi-layer techniques, with copy-paste examples throughout.&lt;/p&gt;
&lt;h2&gt;The Basic Syntax&lt;/h2&gt;
&lt;p&gt;``&lt;code&gt;css&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;box-shadow: offset-x offset-y blur-radius spread-radius color;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Each part explained:&lt;/p&gt;
&lt;p&gt;| Parameter | What it does |&lt;/p&gt;
&lt;p&gt;|---|---|&lt;/p&gt;
&lt;p&gt;| offset-x&lt;code&gt; | Horizontal position. Positive = right, negative = left |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| offset-y&lt;code&gt; | Vertical position. Positive = down, negative = up |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| blur-radius&lt;code&gt; | Edge softness. 0 = crisp, higher = more diffuse |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| spread-radius&lt;code&gt; | Expands (+) or contracts (−) the shadow before blur |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| color&lt;code&gt; | Any valid CSS color, usually &lt;/code&gt;rgba()&lt;code&gt; for opacity control |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;A simple card shadow:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;css
&amp;lt;p&amp;gt;.card {&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;}&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Inset Shadows&lt;/h2&gt;
&lt;p&gt;Add the inset&lt;code&gt; keyword to flip the shadow inside the element — useful for pressed button states and recessed input fields:&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;css&lt;br&gt;
/&lt;em&gt; Pressed button &lt;/em&gt;/&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;.btn:active {&lt;/p&gt;&lt;br&gt;
&lt;p&gt;box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.2);&lt;/p&gt;&lt;br&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Recessed input field &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;input {&lt;/p&gt;
&lt;p&gt;box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Multiple Shadow Layers&lt;/h2&gt;
&lt;p&gt;You can stack multiple shadows with a comma-separated list. The first shadow is drawn on top (closest to the element):&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;css
&amp;lt;p&amp;gt;.card {&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;box-shadow:&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;0 1px 3px rgba(0, 0, 0, 0.12),   /&amp;lt;em&amp;gt; tight inner shadow for crispness &amp;lt;/em&amp;gt;/&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;0 6px 24px rgba(0, 0, 0, 0.08);  /&amp;lt;em&amp;gt; wide outer shadow for ambient depth &amp;lt;/em&amp;gt;/&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;}&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This two-layer pattern mimics how real shadows work — a bright direct light source creates a sharp inner shadow, while ambient light creates a soft outer glow.&lt;/p&gt;
&lt;h2&gt;The Four Elevation Levels&lt;/h2&gt;
&lt;p&gt;Here's a practical elevation scale for UI design:&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;css&lt;br&gt;
/&lt;em&gt; Flat — default state &lt;/em&gt;/&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;.elevation-0 {&lt;/p&gt;&lt;br&gt;
&lt;p&gt;box-shadow: none;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Subtle — cards, sections &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;.elevation-1 {&lt;/p&gt;
&lt;p&gt;box-shadow: 0 1px 3px rgba(0, 0, 0, 0.10), 0 2px 8px rgba(0, 0, 0, 0.06);&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Standard — hover state, dropdowns &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;.elevation-2 {&lt;/p&gt;
&lt;p&gt;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12), 0 8px 24px rgba(0, 0, 0, 0.08);&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Elevated — modals, dialogs &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;.elevation-3 {&lt;/p&gt;
&lt;p&gt;box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14), 0 16px 48px rgba(0, 0, 0, 0.10);&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Glow Effects&lt;/h2&gt;
&lt;p&gt;Set both offsets to 0&lt;code&gt; and use a saturated colour for a glow:&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;css&lt;br&gt;
/&lt;em&gt; Green glow &lt;/em&gt;/&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;.btn-primary:focus {&lt;/p&gt;&lt;br&gt;
&lt;p&gt;box-shadow: 0 0 0 3px rgba(47, 133, 90, 0.4);&lt;/p&gt;&lt;br&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Stacked glow for stronger effect &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;.notification-badge {&lt;/p&gt;
&lt;p&gt;box-shadow:&lt;/p&gt;
&lt;p&gt;0 0 8px rgba(229, 62, 62, 0.5),&lt;/p&gt;
&lt;p&gt;0 0 24px rgba(229, 62, 62, 0.25);&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Using box-shadow as a Border&lt;/h2&gt;
&lt;p&gt;box-shadow&lt;code&gt; can replace a &lt;/code&gt;border&lt;code&gt; without affecting layout — useful for focus rings and outlines:&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;css&lt;br&gt;
/&lt;em&gt; Focus ring that respects border-radius &lt;/em&gt;/&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;button:focus-visible {&lt;/p&gt;&lt;br&gt;
&lt;p&gt;outline: none;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.6);&lt;/p&gt;&lt;br&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; 2px green border without layout shift &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;.selected-card {&lt;/p&gt;
&lt;p&gt;box-shadow: 0 0 0 2px #2f855a;&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Neumorphism&lt;/h2&gt;
&lt;p&gt;The neumorphic trend uses two shadows in opposite directions on a matching background:&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;css&lt;br&gt;
/&lt;em&gt; Element background must match --surface &lt;/em&gt;/&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;:root {&lt;/p&gt;&lt;br&gt;
&lt;p&gt;--surface: #e0e5ec;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;.neumorphic {&lt;/p&gt;
&lt;p&gt;background: var(--surface);&lt;/p&gt;
&lt;p&gt;box-shadow:&lt;/p&gt;
&lt;p&gt;9px 9px 16px #b8bec7,    /&lt;em&gt; dark shadow — bottom-right &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;-9px -9px 16px #ffffff;  /&lt;em&gt; light shadow — top-left &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Pressed state &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;.neumorphic:active {&lt;/p&gt;
&lt;p&gt;box-shadow:&lt;/p&gt;
&lt;p&gt;inset 4px 4px 8px #b8bec7,&lt;/p&gt;
&lt;p&gt;inset -4px -4px 8px #ffffff;&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Animating box-shadow&lt;/h2&gt;
&lt;p&gt;Animating box-shadow&lt;code&gt; directly triggers a repaint on every frame. For smooth hover transitions, it's still acceptable for most UI:&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;css&lt;br&gt;
.card {&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.10);&lt;/p&gt;&lt;br&gt;
&lt;p&gt;transition: box-shadow 0.2s ease, transform 0.2s ease;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;.card:hover {&lt;/p&gt;
&lt;p&gt;box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);&lt;/p&gt;
&lt;p&gt;transform: translateY(-2px);&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For high-frequency animations (scroll-tied effects), animate opacity&lt;code&gt; on a &lt;/code&gt;::after&lt;code&gt; pseudo-element instead — this stays on the compositor layer.&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;box-shadow vs filter: drop-shadow()&lt;/h2&gt;
&lt;p&gt;| | box-shadow&lt;code&gt; | &lt;/code&gt;filter: drop-shadow()&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;|---|---|---|&lt;/p&gt;
&lt;p&gt;| Follows | Element's box | Actual rendered shape |&lt;/p&gt;
&lt;p&gt;| Supports inset&lt;code&gt; | ✅ | ❌ |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| Supports spread | ✅ | ❌ |&lt;/p&gt;
&lt;p&gt;| Multiple layers | ✅ (comma list) | ❌ (wrap in multiple elements) |&lt;/p&gt;
&lt;p&gt;| Best for | Buttons, cards, divs | Transparent PNGs, SVGs |&lt;/p&gt;
&lt;h2&gt;Converting Figma Shadows to CSS&lt;/h2&gt;
&lt;p&gt;Figma's shadow panel maps directly to CSS:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;X&lt;/strong&gt; → offset-x&lt;code&gt;&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Y&lt;/strong&gt; → offset-y&lt;code&gt;&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Blur&lt;/strong&gt; → blur-radius&lt;code&gt;&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Spread&lt;/strong&gt; → spread-radius&lt;code&gt;&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Colour + opacity&lt;/strong&gt; → rgba()&lt;code&gt; or &lt;/code&gt;hsla()&lt;code&gt;&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Inner Shadow&lt;/strong&gt; → add inset&lt;code&gt; keyword&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;
&lt;h2&gt;Quick Reference: Common Patterns&lt;/h2&gt;
&lt;p&gt;`&lt;code&gt;css&lt;br&gt;
/&lt;em&gt; Soft card &lt;/em&gt;/&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;box-shadow: 0 1px 3px rgba(0,0,0,0.10), 0 2px 8px rgba(0,0,0,0.06);&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Material elevation 2 &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 2px 4px rgba(0,0,0,0.08);&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Modal / dialog &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;box-shadow: 0 8px 32px rgba(0,0,0,0.15), 0 20px 64px rgba(0,0,0,0.10);&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Focus ring &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;box-shadow: 0 0 0 3px rgba(66,153,225,0.5);&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Inset (pressed button) &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;box-shadow: inset 0 2px 4px rgba(0,0,0,0.15);&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Neumorphic (on #e0e5ec background) &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;box-shadow: 9px 9px 16px #b8bec7, -9px -9px 16px #ffffff;&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Remove shadow &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;box-shadow: none;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Building Shadows Without Guessing&lt;/h2&gt;
&lt;p&gt;If you'd rather adjust sliders than type raw CSS, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/css-box-shadow-generator/&quot;&gt;CSS Box Shadow Generator&lt;/a&gt; at SnappyTools gives you a live preview with multi-layer support, Figma-style controls, and a one-click copy button. No install, runs 100% in the browser.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The key insight with box-shadow` is that single-layer shadows look flat. Stack a tight, slightly opaque shadow with a wider, more transparent one — and you get depth that actually looks right.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20695/css-box-shadow-the-complete-guide-with-copy-paste-examples</guid>
<pubDate>Tue, 16 Jun 2026 10:08:46 +0000</pubDate>
</item>
<item>
<title>CSS box-shadow Deep Dive: Multi-Layer Shadows and Inset Effects</title>
<link>https://coderlegion.com/20386/css-box-shadow-deep-dive-multi-layer-shadows-and-inset-effects</link>
<description>&lt;p&gt;The &lt;code&gt;box-shadow&lt;/code&gt; property looks simple but hides surprising depth. Here is everything you need to know about multi-layer shadows, inset effects, and production-ready presets.&lt;/p&gt;
&lt;h2&gt;Syntax recap&lt;/h2&gt;
&lt;p&gt;``&lt;code&gt;css&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;box-shadow: [inset] offset-x offset-y [blur] [spread] color;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;offset-x / offset-y&lt;/strong&gt;: direction of the shadow. Negative values go left/up.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;blur&lt;/strong&gt;: soft edge (0 = sharp). Cannot be negative.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;spread&lt;/strong&gt;: expand (+) or shrink (-) the shadow beyond the element.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;inset&lt;/strong&gt;: puts the shadow inside the element instead of outside.&lt;/li&gt;&lt;/ul&gt;
&lt;h2&gt;Multiple layers&lt;/h2&gt;
&lt;p&gt;Comma-separate shadows. First shadow draws on top:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;css
&amp;lt;p&amp;gt;.card {&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;box-shadow:&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;0 1px 3px rgba(0, 0, 0, 0.12),&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;0 8px 32px rgba(0, 0, 0, 0.06);&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;}&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The pattern: one tight, crisp shadow for definition — one wide, soft shadow for ambient depth. This matches how real light behaves.&lt;/p&gt;
&lt;h2&gt;Inset for pressed states&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;css
&amp;lt;p&amp;gt;.btn:active {&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.25);&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;transform: translateY(1px);&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;}&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Combine inset shadow + 1px translateY for a convincing button press.&lt;/p&gt;
&lt;h2&gt;Border replacement&lt;/h2&gt;
&lt;p&gt;box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5)&lt;code&gt; is a zero-blur, zero-offset shadow that acts like an outline — respects &lt;/code&gt;border-radius&lt;code&gt; and doesn't affect layout.&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Glow effect&lt;/h2&gt;
&lt;p&gt;Zero offset + large blur + saturated colour:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;css
&amp;lt;p&amp;gt;.neon {&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;box-shadow:&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;0 0 10px rgba(47, 133, 90, 0.5),&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;0 0 40px rgba(47, 133, 90, 0.2);&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;}&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Quick reference table&lt;/h2&gt;
&lt;p&gt;| Use Case | CSS |&lt;/p&gt;
&lt;p&gt;|----------|-----|&lt;/p&gt;
&lt;p&gt;| Soft card | 0 2px 8px rgba(0,0,0,0.08)&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| Material elevation | 0 1px 3px rgba(0,0,0,0.12), 0 4px 16px rgba(0,0,0,0.08)&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| Focus ring | 0 0 0 3px rgba(66,153,225,0.5)&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| Inset press | inset 0 2px 6px rgba(0,0,0,0.2)&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| Glow | 0 0 20px rgba(47,133,90,0.4)` |&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;You can build and preview all of these visually at &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/css-box-shadow-generator/&quot;&gt;SnappyTools CSS Box Shadow Generator&lt;/a&gt; — sliders for every parameter, multi-layer support, live preview, and one-click CSS copy.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/css-box-shadow-generator/&quot;&gt;https://snappytools.app/css-box-shadow-generator/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/20386/css-box-shadow-deep-dive-multi-layer-shadows-and-inset-effects</guid>
<pubDate>Fri, 12 Jun 2026 10:17:12 +0000</pubDate>
</item>
<item>
<title>CSS Gradients: Linear, Radial, and Conic — Syntax Reference and Real Examples</title>
<link>https://coderlegion.com/19654/css-gradients-linear-radial-and-conic-syntax-reference-and-real-examples</link>
<description>&lt;p&gt;CSS gradients are powerful and flexible, but the syntax is easy to forget if you don't use them every day. Here's a complete reference with real examples.&lt;/p&gt;
&lt;h2&gt;Three Types of CSS Gradients&lt;/h2&gt;
&lt;h3&gt;Linear Gradient&lt;/h3&gt;
&lt;p&gt;``&lt;code&gt;css&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;background: linear-gradient(direction, color1, color2, ...);&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Direction options:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Keywords: to right&lt;code&gt;, &lt;/code&gt;to bottom&lt;code&gt;, &lt;/code&gt;to bottom right&lt;code&gt;&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Angles: 45deg&lt;code&gt;, &lt;/code&gt;135deg&lt;code&gt;, &lt;/code&gt;270deg&lt;code&gt;&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;`&lt;code&gt;css&lt;br&gt;
/&lt;em&gt; Basic &lt;/em&gt;/&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;background: linear-gradient(to right, #2f855a, #68d391);&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Diagonal &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;background: linear-gradient(135deg, #667eea, #764ba2);&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Three stops &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;background: linear-gradient(to right, #f6d365, #fda085, #f093fb);&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Angle reference:&lt;/strong&gt; 0deg&lt;code&gt; = bottom to top, &lt;/code&gt;90deg&lt;code&gt; = left to right, &lt;/code&gt;180deg&lt;code&gt; = top to bottom.&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Radial Gradient&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;css
&amp;lt;p&amp;gt;background: radial-gradient(shape size at position, color1, color2);&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;css&lt;br&gt;
/&lt;em&gt; Circle from center &lt;/em&gt;/&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;background: radial-gradient(circle, #667eea, #764ba2);&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Circle from corner &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;background: radial-gradient(circle at 10% 10%, #a8edea, #fed6e3);&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Conic Gradient&lt;/h3&gt;
&lt;p&gt;Good for pie charts and colour wheels:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;css
&amp;lt;p&amp;gt;/&amp;lt;em&amp;gt; Pie chart: 30% / 50% / 20% &amp;lt;/em&amp;gt;/&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;background: conic-gradient(red 0% 30%, blue 30% 80%, green 80% 100%);&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Hard Stops (No Fade)&lt;/h2&gt;
&lt;p&gt;Position two stops at the same point to create a sharp colour change:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;css
&amp;lt;p&amp;gt;/&amp;lt;em&amp;gt; Striped flag &amp;lt;/em&amp;gt;/&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;background: linear-gradient(to right,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;red 0% 33%,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;white 33% 66%,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;blue 66% 100%&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;);&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Repeating Gradients&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;css
&amp;lt;p&amp;gt;/&amp;lt;em&amp;gt; Diagonal stripes &amp;lt;/em&amp;gt;/&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;background: repeating-linear-gradient(&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;45deg,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;transparent,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;transparent 10px,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;#2f855a 10px,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;#2f855a 20px&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;);&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Common Use Cases&lt;/h2&gt;
&lt;p&gt;| Pattern | CSS |&lt;/p&gt;
&lt;p&gt;|---|---|&lt;/p&gt;
&lt;p&gt;| Hero overlay | linear-gradient(135deg, #667eea, #764ba2)&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| Card darkening | linear-gradient(to bottom, transparent, rgba(0,0,0,0.6))&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| Progress bar fill | linear-gradient(to right, #2f855a, #68d391)&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| Radial spotlight | radial-gradient(circle, rgba(255,255,255,0.3), transparent)&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| Stripe divider | repeating-linear-gradient(45deg, ...)` |&lt;/p&gt;
&lt;h2&gt;Browser Support&lt;/h2&gt;
&lt;p&gt;All three types are supported in all modern browsers (Chrome, Firefox, Safari, Edge) with no vendor prefixes needed.&lt;/p&gt;
&lt;h2&gt;Generate Without Writing It&lt;/h2&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/gradient-generator/&quot;&gt;SnappyTools Gradient Generator&lt;/a&gt; lets you pick colours, adjust direction, and copy the final CSS directly — with a live preview on a real element. Useful when you want to experiment visually before writing code.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Which gradient type do you use most in production? Linear for backgrounds, or do you actually use conic gradients?&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/19654/css-gradients-linear-radial-and-conic-syntax-reference-and-real-examples</guid>
<pubDate>Fri, 12 Jun 2026 02:07:41 +0000</pubDate>
</item>
<item>
<title>CSS Minification: What It Does and When to Use It</title>
<link>https://coderlegion.com/20153/css-minification-what-it-does-and-when-to-use-it</link>
<description>&lt;p&gt;CSS minification removes everything in a stylesheet that the browser does not need — whitespace, comments, redundant semicolons, and shorthand colour values — without changing what any rule does. A typical stylesheet drops 20–40% in size after minification, and that reduction is compounded further when gzip is applied on the server.&lt;/p&gt;
&lt;h2&gt;What gets removed&lt;/h2&gt;
&lt;p&gt;A minifier processes your CSS and strips:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;All whitespace (spaces, tabs, newlines between rules)&lt;/li&gt;&lt;li&gt;Comments (&lt;code&gt;/&lt;em&gt; ... &lt;/em&gt;/&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;Redundant trailing semicolons in rule blocks&lt;/li&gt;&lt;li&gt;Verbose colour notation (&lt;code&gt;#ffffff&lt;/code&gt; → &lt;code&gt;#fff&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;Leading zeros (&lt;code&gt;0.5em&lt;/code&gt; → &lt;code&gt;.5em&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;Zero units (&lt;code&gt;0px&lt;/code&gt; → &lt;code&gt;0&lt;/code&gt;)&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;The resulting CSS is functionally identical to the original. Every selector, property, and value is preserved.&lt;/p&gt;
&lt;h2&gt;How much size reduction to expect&lt;/h2&gt;
&lt;p&gt;For a 100 KB stylesheet:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;After minification: ~65–80 KB&lt;/li&gt;&lt;li&gt;After minification + gzip: ~15–25 KB&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;Minification reduces file size directly. It also improves gzip compression ratios because the compressor encounters a more consistent character pattern. Minification without gzip still helps; gzip without minification helps more; both together give the best result.&lt;/p&gt;
&lt;h2&gt;Build tool integration&lt;/h2&gt;
&lt;p&gt;Modern JavaScript build tools minify CSS automatically in production mode:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Vite:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;p&gt;&lt;code&gt;`&amp;lt;code&amp;gt;bash&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;vite build  # CSS is minified via LightningCSS automatically&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Webpack:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;&amp;lt;code&amp;gt;bash
&amp;lt;h1&amp;gt;css-minimizer-webpack-plugin is included in most webpack templates&amp;lt;/h1&amp;gt;
&amp;lt;h1&amp;gt;It runs automatically when mode: 'production' is set&amp;lt;/h1&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PostCSS + cssnano (explicit):&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;&amp;lt;code&amp;gt;bash
&amp;lt;p&amp;gt;npx postcss styles.css --use cssnano -o styles.min.css&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For projects without a build pipeline, minify the file once manually before deploy. For static sites maintained without tooling, use an online minifier like the &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/css-minifier-beautifier/&quot;&gt;CSS Minifier &amp;amp; Beautifier on SnappyTools&lt;/a&gt; — paste the stylesheet, click Minify, copy the result.&lt;/p&gt;
&lt;h2&gt;Beautifying minified CSS&lt;/h2&gt;
&lt;p&gt;When you need to read a minified file (vendor code, a received production bundle, a legacy file without a source), beautify it first. Beautification is the reverse operation: it restores indentation, adds newlines between rules, and formats output consistently.&lt;/p&gt;
&lt;p&gt;Beautified CSS is for reading only — re-minify before deploying if you make changes.&lt;/p&gt;
&lt;h2&gt;Source maps&lt;/h2&gt;
&lt;p&gt;If you are debugging a production issue in minified CSS, use source maps. Most minifiers support --source-map` flags. Source maps let browser DevTools map minified positions back to the original source file and line number, making CSS debugging viable without de-minifying.&lt;/p&gt;
&lt;h2&gt;When minification does not matter&lt;/h2&gt;
&lt;p&gt;For internal tools, admin panels, or sites with a single small stylesheet (under 20 KB), minification has a negligible impact. Spend optimisation effort on images and JavaScript first; CSS rarely contributes more than 5–10% of total page weight on modern sites.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Minify or beautify a CSS file without a build tool at &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/css-minifier-beautifier/&quot;&gt;snappytools.app/css-minifier-beautifier/&lt;/a&gt; — runs entirely in your browser.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/css-minifier-beautifier/&quot;&gt;&lt;/a&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/css-minifier-beautifier/&amp;lt;/a&amp;gt;&amp;lt;/em&amp;gt;&amp;lt;/p&amp;gt;&quot;&gt;https://snappytools.app/css-minifier-beautifier/&amp;lt;/a&amp;gt;&amp;lt;/em&amp;gt;&amp;lt;/p&amp;gt;&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20153/css-minification-what-it-does-and-when-to-use-it</guid>
<pubDate>Wed, 10 Jun 2026 10:12:15 +0000</pubDate>
</item>
<item>
<title>CSS Gradients: A Complete Guide for Developers and Designers</title>
<link>https://coderlegion.com/19951/css-gradients-a-complete-guide-for-developers-and-designers</link>
<description>&lt;p&gt;CSS gradients let you create smooth transitions between colours without image files — they're resolution-independent, load instantly, and are supported in every modern browser. If you've been avoiding gradients because the syntax looks intimidating, this guide breaks it down step by step.&lt;/p&gt;
&lt;h2&gt;The Three Types of CSS Gradient&lt;/h2&gt;
&lt;h3&gt;1. Linear Gradient&lt;/h3&gt;
&lt;p&gt;The most common type. Colour transitions along a straight line.&lt;/p&gt;
&lt;p&gt;``&lt;code&gt;css&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Basic top-to-bottom &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;background: linear-gradient(#6c63ff, #f43f8a);&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; With direction &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;background: linear-gradient(to right, #6c63ff, #f43f8a);&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; With angle &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;background: linear-gradient(135deg, #6c63ff, #f43f8a);&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The angle controls direction: 0deg&lt;code&gt; is bottom-to-top, &lt;/code&gt;90deg&lt;code&gt; is left-to-right, &lt;/code&gt;180deg&lt;code&gt; is top-to-bottom. The most popular choice in modern UI design is &lt;/code&gt;135deg&lt;code&gt; — a diagonal that feels dynamic without being aggressive.&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;2. Radial Gradient&lt;/h3&gt;
&lt;p&gt;Transitions outward from a centre point, creating circular or elliptical patterns.&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;css&lt;br&gt;
/&lt;em&gt; Default — ellipse from centre &lt;/em&gt;/&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;background: radial-gradient(#6c63ff, #f43f8a);&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Circle, positioned top-left &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;background: radial-gradient(circle at 20% 30%, #6c63ff, transparent);&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Radial gradients are useful for spotlight effects, glow accents, and the mesh gradient technique (stacking multiple radials at different positions).&lt;/p&gt;
&lt;h3&gt;3. Conic Gradient&lt;/h3&gt;
&lt;p&gt;Transitions around a centre point, like a pie chart or colour wheel.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;css
&amp;lt;p&amp;gt;background: conic-gradient(red, orange, yellow, green, blue, violet, red);&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Conic gradients are perfect for progress rings, pie charts, and colour wheels.&lt;/p&gt;
&lt;h2&gt;Adding Multiple Colour Stops&lt;/h2&gt;
&lt;p&gt;Gradients aren't limited to two colours. Add as many stops as you need:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;css
&amp;lt;p&amp;gt;background: linear-gradient(135deg, #6c63ff 0%, #f43f8a 50%, #ff7700 100%);&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Stop positions can be percentages or lengths (px&lt;code&gt;, &lt;/code&gt;em&lt;code&gt;). If you omit positions, the browser distributes stops evenly.&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Creating a Gradient Overlay on an Image&lt;/h2&gt;
&lt;p&gt;One of the most practical uses of gradients is overlaying them on photos to keep text legible:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;css
&amp;lt;p&amp;gt;.hero {&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;background:&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.1)),&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;url('photo.jpg') center / cover no-repeat;&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;}&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The gradient is listed first so it renders on top. Replace the rgba&lt;code&gt; values with a brand colour for a tinted overlay.&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Rainbow Gradient&lt;/h2&gt;
&lt;p&gt;A full-spectrum rainbow uses all seven spectral colours as stops:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;css
&amp;lt;p&amp;gt;background: linear-gradient(90deg, #ff0000, #ff7700, #ffff00, #00cc00, #0000ff, #8b00ff);&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For a smoother transition, add more intermediate stops between primary colours.&lt;/p&gt;
&lt;h2&gt;Animating a Gradient&lt;/h2&gt;
&lt;p&gt;CSS can't directly interpolate between gradient stops, but you can simulate animation using background-size&lt;code&gt; and &lt;/code&gt;background-position&lt;code&gt;:&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;css&lt;br&gt;
.animated {&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;background: linear-gradient(270deg, #6c63ff, #f43f8a, #6c63ff);&lt;/p&gt;&lt;br&gt;
&lt;p&gt;background-size: 400% 400%;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;animation: gradientShift 4s ease infinite;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;@keyframes gradientShift {&lt;/p&gt;
&lt;p&gt;0% { background-position: 0% 50%; }&lt;/p&gt;
&lt;p&gt;50% { background-position: 100% 50%; }&lt;/p&gt;
&lt;p&gt;100% { background-position: 0% 50%; }&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This creates a looping colour shift with pure CSS — no JavaScript required.&lt;/p&gt;
&lt;h2&gt;Applying a Gradient to Text&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;css
&amp;lt;p&amp;gt;.gradient-text {&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;background: linear-gradient(135deg, #6c63ff, #f43f8a);&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;-webkit-background-clip: text;&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;-webkit-text-fill-color: transparent;&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;background-clip: text;&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;}&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The -webkit-&lt;code&gt; prefixes are still needed for Safari compatibility.&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Using CSS Custom Properties with Gradients&lt;/h2&gt;
&lt;p&gt;`&lt;code&gt;css&lt;br&gt;
:root {&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;--grad-start: #6c63ff;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;--grad-end: #f43f8a;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;.hero {&lt;/p&gt;
&lt;p&gt;background: linear-gradient(135deg, var(--grad-start), var(--grad-end));&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;@media (prefers-color-scheme: dark) {&lt;/p&gt;
&lt;p&gt;:root {&lt;/p&gt;
&lt;p&gt;--grad-start: #3b1fa8;&lt;/p&gt;
&lt;p&gt;--grad-end: #a0155a;&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Mesh Gradients&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;css
&amp;lt;p&amp;gt;.mesh {&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;background:&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;radial-gradient(at 20% 30%, #6c63ff 0px, transparent 50%),&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;radial-gradient(at 80% 70%, #f43f8a 0px, transparent 50%),&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;radial-gradient(at 50% 50%, #68d391 0px, transparent 60%);&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;background-color: #0f172a;&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;}&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Quick Reference: Linear Gradient Angles&lt;/h2&gt;
&lt;p&gt;| Angle | Direction | CSS Keyword |&lt;/p&gt;
&lt;p&gt;|-------|-----------|-------------|&lt;/p&gt;
&lt;p&gt;| 0deg | Bottom → Top | to top&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| 90deg | Left → Right | to right&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| 135deg | Bottom-left → Top-right | to top right&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| 180deg | Top → Bottom | to bottom&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;| 270deg | Right → Left | to left&lt;code&gt; |&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Tools&lt;/h2&gt;
&lt;p&gt;Use the &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/gradient-generator/&quot;&gt;CSS Gradient Generator on SnappyTools&lt;/a&gt;&lt;/strong&gt; — add colour stops, adjust the angle, switch between linear/radial/conic, and copy the ready-to-use CSS in one click. Free, no account needed.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;ul&gt;&lt;li&gt;Use linear-gradient&lt;code&gt; for directional fades and hero sections&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Use radial-gradient&lt;code&gt; for spotlights, glows, and mesh effects&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Use conic-gradient&lt;code&gt; for charts and colour wheels&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Animate with background-size&lt;code&gt; + &lt;/code&gt;background-position` — no JavaScript needed&lt;/li&gt;&lt;li&gt;Use CSS custom properties for themeable gradients in design systems&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/gradient-generator/&quot;&gt;https://snappytools.app/gradient-generator/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19951/css-gradients-a-complete-guide-for-developers-and-designers</guid>
<pubDate>Mon, 08 Jun 2026 10:15:50 +0000</pubDate>
</item>
<item>
<title>CSS Gradient Animations: How to Animate Gradients That CSS Can't Directly Animate</title>
<link>https://coderlegion.com/19755/css-gradient-animations-how-to-animate-gradients-that-css-cant-directly-animate</link>
<description>&lt;p&gt;CSS can't interpolate gradient colours directly — &lt;code&gt;transition: background&lt;/code&gt; has no effect on gradients. Here are three practical techniques for animating CSS gradients.&lt;/p&gt;
&lt;h2&gt;Why Transitions Don't Work on Gradients&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;background: linear-gradient(...)&lt;/code&gt; is treated as an image value by CSS, not a colour. You can't transition between two image values, so the browser just snaps. Here are the workarounds.&lt;/p&gt;
&lt;h2&gt;Technique 1: Background-Position Shift (Pure CSS)&lt;/h2&gt;
&lt;p&gt;Make the gradient oversized and animate its position:&lt;/p&gt;
&lt;p&gt;``&lt;code&gt;css&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;.animated {&lt;/p&gt;
&lt;p&gt;background: linear-gradient(270deg, #6c63ff, #f43f8a, #68d391, #6c63ff);&lt;/p&gt;
&lt;p&gt;background-size: 400% 400%;&lt;/p&gt;
&lt;p&gt;animation: shift 6s ease infinite;&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;@keyframes shift {&lt;/p&gt;
&lt;p&gt;0%   { background-position: 0% 50%; }&lt;/p&gt;
&lt;p&gt;50%  { background-position: 100% 50%; }&lt;/p&gt;
&lt;p&gt;100% { background-position: 0% 50%; }&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The gradient is 4× the element width. Animating the position creates a smooth looping effect. Duplicate the start colour at the end for seamless looping.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; Hero sections, button hovers, card backgrounds.&lt;/p&gt;
&lt;h2&gt;Technique 2: Pseudo-Element Crossfade&lt;/h2&gt;
&lt;p&gt;Layer two gradients on pseudo-elements and fade between them:&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;css&lt;br&gt;
.crossfade { position: relative; }&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;.crossfade::before,&lt;/p&gt;
&lt;p&gt;.crossfade::after {&lt;/p&gt;
&lt;p&gt;content: '';&lt;/p&gt;
&lt;p&gt;position: absolute;&lt;/p&gt;
&lt;p&gt;inset: 0;&lt;/p&gt;
&lt;p&gt;border-radius: inherit;&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;.crossfade::before {&lt;/p&gt;
&lt;p&gt;background: linear-gradient(135deg, #6c63ff, #f43f8a);&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;.crossfade::after {&lt;/p&gt;
&lt;p&gt;background: linear-gradient(135deg, #f43f8a, #68d391);&lt;/p&gt;
&lt;p&gt;animation: fade 3s ease-in-out infinite alternate;&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;@keyframes fade {&lt;/p&gt;
&lt;p&gt;0%   { opacity: 0; }&lt;/p&gt;
&lt;p&gt;100% { opacity: 1; }&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This looks like a true colour transition. No JavaScript needed, but uses both pseudo-elements.&lt;/p&gt;
&lt;h2&gt;Technique 3: CSS Custom Properties + JavaScript&lt;/h2&gt;
&lt;p&gt;For full control, use CSS variables and update them each frame:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code&amp;gt;css
&amp;lt;p&amp;gt;.dynamic {&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;background: linear-gradient(&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;var(--angle, 135deg),&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;var(--c1, #6c63ff),&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;var(--c2, #f43f8a)&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;);&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;}&amp;lt;/p&amp;gt;
&amp;lt;/code&amp;gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;javascript&lt;br&gt;
let hue = 0;&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;const el = document.querySelector('.dynamic');&lt;/p&gt;
&lt;p&gt;function tick() {&lt;/p&gt;
&lt;p&gt;hue = (hue + 0.5) % 360;&lt;/p&gt;
&lt;p&gt;el.style.setProperty('--c1', hsl(${hue}, 70%, 60%)&lt;code&gt;);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;el.style.setProperty('--c2', hsl(${(hue + 120) % 360}, 70%, 60%)&lt;code&gt;);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;requestAnimationFrame(tick);&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;tick();&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This gives you complete control over all gradient parameters every frame. Requires JavaScript but works in all modern browsers.&lt;/p&gt;
&lt;h2&gt;Animating the Angle (Houdini)&lt;/h2&gt;
&lt;p&gt;To animate the gradient angle with pure CSS, register the custom property:&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;css&lt;br&gt;
@property --angle {&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;syntax: &quot;&quot;;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;initial-value: 0deg;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;inherits: false;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;.rotating {&lt;/p&gt;
&lt;p&gt;background: conic-gradient(from var(--angle), #6c63ff, #f43f8a, #6c63ff);&lt;/p&gt;
&lt;p&gt;animation: spin 4s linear infinite;&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;@keyframes spin {&lt;/p&gt;
&lt;p&gt;to { --angle: 360deg; }&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Without @property&lt;code&gt;, the animation jumps instead of rotating smoothly. Currently supported in Chrome and Edge only.&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Performance Tips&lt;/h2&gt;
&lt;p&gt;Gradient animations trigger repaints. Keep them fast:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Use will-change: background` on animated elements&lt;/li&gt;&lt;li&gt;Limit to a few animated elements per page&lt;/li&gt;&lt;li&gt;Prefer Technique 1 for simple loops — it's the most GPU-friendly&lt;/li&gt;&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Start with the gradient CSS from the &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/gradient-generator/&quot;&gt;CSS Gradient Generator&lt;/a&gt; at SnappyTools, then wrap it in the animation code above.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/19755/css-gradient-animations-how-to-animate-gradients-that-css-cant-directly-animate</guid>
<pubDate>Fri, 05 Jun 2026 10:12:20 +0000</pubDate>
</item>
<item>
<title>CSS Minification: What Gets Stripped and What Stays</title>
<link>https://coderlegion.com/19470/css-minification-what-gets-stripped-and-what-stays</link>
<description>&lt;p&gt;CSS minification is one of those optimisations that's easy to apply and genuinely makes a difference for page load speed. But it's worth knowing exactly what a minifier does — and doesn't — change about your stylesheet.&lt;/p&gt;
&lt;h2&gt;What a minifier removes&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Comments&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;All &lt;code&gt;/&lt;em&gt; block comments &lt;/em&gt;/&lt;/code&gt; are stripped. The one exception: comments starting with &lt;code&gt;/*!&lt;/code&gt; are preserved by convention — they hold licence notices that must appear in distributed files.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Whitespace&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every space, tab, and newline that CSS doesn't need for correctness is removed. That includes:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Spaces around &lt;code&gt;:&lt;/code&gt;, &lt;code&gt;{&lt;/code&gt;, &lt;code&gt;}&lt;/code&gt;, &lt;code&gt;,&lt;/code&gt;, and &lt;code&gt;;&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Line breaks between rules&lt;/li&gt;&lt;li&gt;Indentation&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Trailing semicolons&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The last property in any &lt;code&gt;{ }&lt;/code&gt; block doesn't need a semicolon. Minifiers remove it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Redundant units on zero&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;margin: 0px&lt;/code&gt; becomes &lt;code&gt;margin:0&lt;/code&gt;. The &lt;code&gt;px&lt;/code&gt; is meaningless when the value is zero.&lt;/p&gt;
&lt;h2&gt;What a minifier keeps&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;All your CSS rules.&lt;/strong&gt; Every selector, property, and value. Minification never removes live code — only syntax around it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Vendor prefixes.&lt;/strong&gt; &lt;code&gt;-webkit-&lt;/code&gt;, &lt;code&gt;-moz-&lt;/code&gt;, &lt;code&gt;-ms-&lt;/code&gt; are kept exactly as written.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;At-rules.&lt;/strong&gt; &lt;code&gt;@media&lt;/code&gt;, &lt;code&gt;@keyframes&lt;/code&gt;, &lt;code&gt;@import&lt;/code&gt;, &lt;code&gt;@charset&lt;/code&gt; are all preserved.&lt;/p&gt;
&lt;h2&gt;Minification vs tree-shaking&lt;/h2&gt;
&lt;p&gt;These are different optimisations:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Minification&lt;/strong&gt; — keeps all rules, compresses syntax&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Tree-shaking&lt;/strong&gt; (PurgeCSS, Tailwind JIT) — removes CSS rules that are never referenced in your HTML/JS&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;For utility-heavy frameworks like Bootstrap or Tailwind, tree-shaking produces much larger savings than minification alone. Do both: tree-shake first, then minify the result.&lt;/p&gt;
&lt;h2&gt;Typical savings&lt;/h2&gt;
&lt;p&gt;| Scenario | Typical reduction |&lt;/p&gt;
&lt;p&gt;|----------|------------------|&lt;/p&gt;
&lt;p&gt;| Handwritten CSS with comments | 25–40% |&lt;/p&gt;
&lt;p&gt;| Already compact CSS | 5–15% |&lt;/p&gt;
&lt;p&gt;| After Brotli on top | Additional 60–70% |&lt;/p&gt;
&lt;h2&gt;How to minify&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Build tools&lt;/strong&gt;: Vite, webpack (CssMinimizerWebpackPlugin), PostCSS + cssnano — all handle this automatically in production builds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CLI&lt;/strong&gt;: &lt;code&gt;npx lightningcss --minify input.css -o output.min.css&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One-off&lt;/strong&gt;: The &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/css-minifier-beautifier/&quot;&gt;CSS Minifier on SnappyTools&lt;/a&gt; lets you paste CSS, see instant results with a bytes-saved counter, and copy the output. It also has a Beautify tab for when you receive minified CSS and need to read it. Runs entirely in your browser — nothing is uploaded.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Minification is the safest performance optimisation in the CSS toolbox. Knowing it only removes whitespace and comments (never logic) makes it a no-brainer step before every production deploy.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19470/css-minification-what-gets-stripped-and-what-stays</guid>
<pubDate>Tue, 02 Jun 2026 10:18:09 +0000</pubDate>
</item>
<item>
<title>CSS Linear Gradient Angles: The Complete Reference</title>
<link>https://coderlegion.com/19469/css-linear-gradient-angles-the-complete-reference</link>
<description>&lt;p&gt;CSS gradients confuse a lot of developers because the angle system doesn't work the way you'd expect from school trigonometry. Once you understand the pattern, every gradient you see becomes readable — and every gradient you build becomes intentional.&lt;/p&gt;
&lt;h2&gt;The CSS angle system&lt;/h2&gt;
&lt;p&gt;In CSS, gradient angles measure direction from &lt;strong&gt;12 o'clock (straight up)&lt;/strong&gt;, increasing clockwise. This is different from standard math angles, which measure from 3 o'clock increasing counter-clockwise.&lt;/p&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;code&gt;0deg&lt;/code&gt; → bottom to top&lt;/li&gt;&lt;li&gt;&lt;code&gt;90deg&lt;/code&gt; → left to right&lt;/li&gt;&lt;li&gt;&lt;code&gt;180deg&lt;/code&gt; → top to bottom&lt;/li&gt;&lt;li&gt;&lt;code&gt;270deg&lt;/code&gt; → right to left&lt;/li&gt;&lt;/ul&gt;
&lt;h2&gt;The 8 standard angles&lt;/h2&gt;
&lt;p&gt;| Angle | Direction | CSS keyword equivalent |&lt;/p&gt;
&lt;p&gt;|-------|-----------|----------------------|&lt;/p&gt;
&lt;p&gt;| 0deg | Bottom → Top | &lt;code&gt;to top&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;| 45deg | Bottom-left → Top-right | &lt;code&gt;to top right&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;| 90deg | Left → Right | &lt;code&gt;to right&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;| 135deg | Top-left → Bottom-right | &lt;code&gt;to bottom right&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;| 180deg | Top → Bottom | &lt;code&gt;to bottom&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;| 225deg | Top-right → Bottom-left | &lt;code&gt;to bottom left&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;| 270deg | Right → Left | &lt;code&gt;to left&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;| 315deg | Bottom-right → Top-left | &lt;code&gt;to top left&lt;/code&gt; |&lt;/p&gt;
&lt;h2&gt;Why 135deg is everywhere&lt;/h2&gt;
&lt;p&gt;Browse any modern SaaS landing page and you'll see &lt;code&gt;135deg&lt;/code&gt; gradients. It's the &quot;default diagonal&quot; — enough movement to feel dynamic, but balanced enough not to feel chaotic. Purple-to-pink, blue-to-teal, and orange-to-red gradients all look natural at 135deg.&lt;/p&gt;
&lt;p&gt;``&lt;code&gt;css&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; The classic &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;background: linear-gradient(135deg, #6c63ff 0%, #f43f8a 100%);&lt;/p&gt;
&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Keyword vs degree&lt;/h2&gt;
&lt;p&gt;CSS keywords like to bottom right&lt;code&gt; point toward the corner of the element, so the actual angle depends on the element's aspect ratio. A 100x100px element with &lt;/code&gt;to bottom right&lt;code&gt; uses exactly 135deg, but a 200x100px element uses a different angle.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For consistency, especially in tight designs, use explicit degree values.&lt;/p&gt;
&lt;h2&gt;Practical examples&lt;/h2&gt;
&lt;p&gt;`&lt;code&gt;css&lt;br&gt;
/&lt;em&gt; Hero section — top to bottom &lt;/em&gt;/&lt;/code&gt;&lt;/p&gt;&lt;br&gt;
&lt;p&gt;background: linear-gradient(180deg, #1e3a8a 0%, #0f172a 100%);&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Progress bar — left to right &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;background: linear-gradient(90deg, #2f855a, #68d391);&lt;/p&gt;
&lt;p&gt;/&lt;em&gt; Button — subtle diagonal &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;background: linear-gradient(135deg, #276749, #2f855a);&lt;/p&gt;
&lt;p&gt;``&lt;/p&gt;
&lt;h2&gt;Building gradients visually&lt;/h2&gt;
&lt;p&gt;If you prefer to experiment visually before writing CSS, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://snappytools.app/gradient-generator/&quot;&gt;CSS Gradient Generator on SnappyTools&lt;/a&gt; has an angle slider, multi-stop colour pickers, and outputs ready-to-paste CSS for linear, radial, and conic gradients. It also exports Tailwind arbitrary-value classes directly.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Gradient angles become intuitive quickly — once you remember that 0deg points up and values go clockwise, the rest follows. Keep 135deg in your muscle memory as the go-to diagonal, and reach for 90deg and 180deg for UI bars and page sections.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19469/css-linear-gradient-angles-the-complete-reference</guid>
<pubDate>Tue, 02 Jun 2026 10:15:18 +0000</pubDate>
</item>
</channel>
</rss>