<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ilyès</title>
    <description>The latest articles on DEV Community by Ilyès (@hdzilyes).</description>
    <link>https://dev.to/hdzilyes</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2395247%2Fd1209cbd-e2f1-47b5-8213-394864de4d0f.jpg</url>
      <title>DEV Community: Ilyès</title>
      <link>https://dev.to/hdzilyes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hdzilyes"/>
    <language>en</language>
    <item>
      <title>The evolution of how we use CSS</title>
      <dc:creator>Ilyès</dc:creator>
      <pubDate>Sat, 18 Jul 2026 18:44:13 +0000</pubDate>
      <link>https://dev.to/hdzilyes/the-evolution-of-how-we-use-css-3ojo</link>
      <guid>https://dev.to/hdzilyes/the-evolution-of-how-we-use-css-3ojo</guid>
      <description>&lt;p&gt;CSS has been around for about 30 years. It is the only styling language built specifically for the web platform, and it is one of the three core technologies that make the web what it is. Without it, every page would be a block of black text on a white background, laid out from top to bottom with no control whatsoever. That is not an exaggeration. That is what the web looks like without CSS.&lt;/p&gt;

&lt;p&gt;The original design goal of CSS has never changed. It is a declarative language that describes how documents should be presented. It does one thing and it does it in a standardized way that works across browsers. That restraint is not a weakness. It is the reason CSS has survived for two decades without being replaced. It never tried to be more than a styling language.&lt;/p&gt;

&lt;p&gt;Looking back at frontend development in the late 2000s, the frustration is hard to overstate. The gap between what CSS could do and what designs required was so wide that the platform itself felt like the obstacle. The vendor prefix era is the clearest example. You wrote &lt;code&gt;-webkit-&lt;/code&gt;, &lt;code&gt;-moz-&lt;/code&gt;, &lt;code&gt;-ms-&lt;/code&gt;, &lt;code&gt;-o-&lt;/code&gt; before every experimental property, often all four, because no browser could agree on when a feature was stable. Autoprefixer became a standard dependency not because developers were lazy, but because manual prefix management was genuinely unsustainable. It was not that CSS was badly designed. It was that the pace of the platform could not keep up with what developers were building.&lt;/p&gt;

&lt;p&gt;This created a pattern. Every time the platform fell short, the community built a workaround. Those workarounds became tools. Those tools became dependencies. And those dependencies reshaped how we thought about CSS entirely. Each new abstraction solved a real problem, but it also moved us further from writing actual CSS. Eventually, it became natural to assume that any serious project needed a layer on top of CSS to be viable.&lt;/p&gt;

&lt;p&gt;However, if CSS is so good at its job, why have we spent so long building alternative ecosystems on top of it? Preprocessors, CSS-in-JS libraries, utility frameworks. The list keeps growing. Every one of them claims to fix something that CSS was supposedly doing wrong.&lt;/p&gt;

&lt;p&gt;The answer is that CSS was never wrong. It was incomplete. And for a long time, the only way to get the features we needed was to build them ourselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The preprocessor era: Sass and the missing features
&lt;/h2&gt;

&lt;p&gt;Sass was the first serious attempt to extend CSS with programming language features. At the time, CSS had no variables, no nesting, no mixins, no way to split your styles into multiple files without terrible performance. You repeated the same color value across your entire stylesheet. You wrote the full selector path for every nested element. You copied and pasted vendor prefixes by hand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nv"&gt;$primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#3498db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$padding&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nc"&gt;.card-header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="mi"&gt;.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$primary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sass became the standard because it solved real problems. The SCSS syntax made the transition from CSS trivial. Every valid CSS file was already valid SCSS. Frameworks like Bootstrap built their entire ecosystem on it. For years, starting a serious frontend project without Sass felt irresponsible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom properties: the first crack
&lt;/h3&gt;

&lt;p&gt;However, the web platform was watching. CSS custom properties (variables) eventually landed in browsers. That alone removed one of the biggest reasons to reach for a preprocessor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#3498db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--padding&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What most people do not realize is that custom properties took so long because they are fundamentally different from Sass variables. Sass variables are compiled away at build time. They do not exist in the browser. CSS custom properties cascade, inherit, and are available at runtime. You can change them with JavaScript, override them inside media queries, or set them dynamically based on user interaction. That flexibility required the CSS working group to solve cascade resolution problems that a build-time tool never had to face. The implementation was harder, but the result is more powerful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nesting goes native
&lt;/h3&gt;

&lt;p&gt;Then came native nesting. Now every major browser supports it. After depending on Sass for nested selectors for over a decade and a half, the browser can finally do it natively.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;.card-header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="mi"&gt;.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The tooling ecosystem
&lt;/h3&gt;

&lt;p&gt;The preprocessor era was also about the tooling ecosystem that grew around it. Tools like PostCSS and Autoprefixer changed how I thought about browser support. Instead of waiting for every browser to implement a feature consistently, I could write modern CSS and let the tooling handle backward compatibility. That was a profound shift. It separated the CSS I write from the CSS the browser receives. Once that separation existed, adding a preprocessing step felt natural rather than exotic.&lt;/p&gt;

&lt;p&gt;The same era gave us naming conventions like BEM as workarounds for the lack of real scoping in CSS. BEM was not a tool. It was a discipline. You manually namespaced every class to avoid collisions. &lt;code&gt;.block__element--modifier&lt;/code&gt;. It worked, but it was tedious. You were effectively writing a scoping system by hand, one class name at a time.&lt;/p&gt;

&lt;p&gt;Looking back, I do not think Sass would have become as universal without Node.js changing how we built frontend applications. Once build steps became normal, preprocessing CSS stopped feeling like an exotic choice and started feeling like part of everyday development. Grunt, Gulp, and later Webpack gave developers a way to process assets before they reached the browser. Once you had a build step for JavaScript, adding one for CSS was trivial. The infrastructure that made preprocessing universal was never about CSS. It was about how frontend development itself was evolving.&lt;/p&gt;

&lt;p&gt;Sass is not dead. It is still actively maintained and widely used. But the argument for using it on new projects has gotten noticeably weaker. Variables are native. Nesting is native. Cascade layers, &lt;code&gt;color-mix()&lt;/code&gt;, container queries. The features that once required &lt;code&gt;@mixin&lt;/code&gt; and &lt;code&gt;@include&lt;/code&gt; are increasingly available in plain CSS.&lt;/p&gt;

&lt;p&gt;Preprocessors were not a mistake. They were a bridge. And like every bridge, once you cross it, you do not need to keep living on it.&lt;/p&gt;

&lt;p&gt;However, preprocessing was still about extending CSS syntax. The next generation of tools approached the problem from a completely different direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  CSS-in-JS: runtime, build-time, and the object problem
&lt;/h2&gt;

&lt;p&gt;Styled-components changed how a generation of React developers thought about styles. The idea was elegant. Write actual CSS inside your JavaScript file, colocate styles with components, access props directly, and never worry about class name collisions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;styled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="s2"&gt;`
  background: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#3498db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transparent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;;
  color: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;white&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#3498db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;;
  padding: 0.75rem 1.5rem;
  border: 2px solid #3498db;
  border-radius: 6px;

  &amp;amp;:hover {
    opacity: 0.9;
  }
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The runtime cost
&lt;/h3&gt;

&lt;p&gt;It felt like magic. And like most magic, it had a hidden cost. Styled-components runs in the browser. Every styled element is parsed, hashed, and injected into the DOM at runtime. If JavaScript fails to load, your styles do not exist. If you are on a slow device, the style injection competes with your application code. Pages that work perfectly on a developer's MacBook stutter on a mid-range Android phone.&lt;/p&gt;

&lt;p&gt;The performance cost is easy to overlook when you develop on a modern machine. Runtime CSS-in-JS means every style definition must be serialized, parsed, and inserted as a &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tag during JavaScript execution. The browser's own CSS parser is written in C++ and runs before JavaScript. When you bypass it by generating styles in JavaScript, you lose that performance advantage entirely. On top of that, the library itself adds meaningful overhead to your bundle. That is not a dealbreaker for every project, but it is pure cost that does nothing except enable the abstraction itself.&lt;/p&gt;

&lt;p&gt;Then there is the hydration problem. When you server-render styled-components, the server generates class names and injects critical CSS into the response. On the client, the runtime must generate the same class names and reconcile them. If the hashes do not match, you get a flash of unstyled content or a full re-render. This is not a bug. It is a fundamental tension between generating styles at build time or server time and regenerating them on the client.&lt;/p&gt;

&lt;p&gt;The deeper problem is architectural. Runtime CSS-in-JS fundamentally depends on JavaScript being present and executing in the browser. For React Server Components, where components run entirely on the server, this model breaks completely. The React team has been clear about this. Runtime CSS-in-JS is not compatible with the server component model, and there is no path to make it compatible without fundamentally changing how it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  CSS Modules: the quiet alternative
&lt;/h3&gt;

&lt;p&gt;I have always felt that CSS Modules deserved more attention than they got. The idea is simple: write plain CSS in a &lt;code&gt;.module.css&lt;/code&gt; file, and the build tool scopes every class name automatically by adding a unique hash. No runtime. No JavaScript dependency. No new syntax to learn. It is just CSS that happens to be scoped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* Button.module.css */&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="nx"&gt;rem&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="nx"&gt;rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;px&lt;/span&gt; &lt;span class="nx"&gt;solid&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;3498&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;border&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="nx"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;3498&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Button.module.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CSS Modules solved scoping without any of the runtime overhead. They never got the hype of styled-components because they did not offer anything flashy. They just worked. And for many teams, that was enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build-time CSS-in-JS
&lt;/h3&gt;

&lt;p&gt;The limitations of runtime CSS-in-JS led to a new category of tools: build-time CSS-in-JS. Panda CSS is the most notable example. It reads your source files at build time, extracts every style call, and generates a plain CSS file before anything reaches the browser. Zero runtime overhead. Works with server components. Type-safe design tokens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./styled-system/css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;flexDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;column&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gray.100&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bold&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Hello Panda
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I appreciate what Panda is trying to do. It solves the runtime problem completely. The generated CSS is clean, modern, and uses cascade layers. But I cannot bring myself to love it. Because look at that syntax. Property names in camelCase, values as strings, no semicolons, no selectors, no cascade. To me, it stops feeling like CSS. It is CSS translated through a JavaScript lens, and that translation loses something fundamental.&lt;/p&gt;

&lt;p&gt;CSS was designed to be the best way to describe visual design on the web. Every part of its syntax, the selectors, the cascade, the shorthand properties, was built for that purpose. When you force it into object syntax, you lose the expressiveness that makes CSS what it is. You cannot write &lt;code&gt;.card h2 + p&lt;/code&gt; in an object. You cannot use the cascade in any meaningful way. Personally, this syntax loses much of what I enjoy about writing CSS.&lt;/p&gt;

&lt;p&gt;The irony is that build-time extraction is the right idea, but the syntax it enables is the wrong interface. What we actually need is not CSS expressed in JavaScript objects. It is CSS that can do the things we needed JavaScript for in the first place.&lt;/p&gt;

&lt;p&gt;The tension here is that CSS-in-JS solved real problems. Co-location matters. Scoped styles matter. Type safety in styles matters. But the solutions have always felt like workarounds, and the closer native CSS gets to providing these features directly, the less necessary the abstraction becomes. &lt;code&gt;@scope&lt;/code&gt; for scoping, container queries for context-aware styles, &lt;code&gt;:has()&lt;/code&gt; for parent selection. The platform is catching up.&lt;/p&gt;

&lt;p&gt;Meanwhile, a completely different philosophy was reshaping how developers approached CSS entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The utility-first shift: Tailwind CSS
&lt;/h2&gt;

&lt;p&gt;No discussion of modern CSS usage is complete without talking about Tailwind CSS. It is, by any measure, one of the most influential CSS tools ever created.&lt;/p&gt;

&lt;p&gt;Tailwind's core insight is simple. Instead of writing custom CSS for every component, use a set of predefined utility classes. You want flexbox? Write &lt;code&gt;flex&lt;/code&gt;. You want padding? Write &lt;code&gt;p-4&lt;/code&gt;. You want it to be responsive? Add &lt;code&gt;md:p-6&lt;/code&gt;. The approach eliminates context switching, removes the need to name things, and enforces consistency across your entire codebase.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col md:flex-row gap-4 p-6 bg-white rounded-lg shadow-md"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full md:w-48 h-48 object-cover rounded-lg"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"photo.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Photo"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col gap-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-xl font-semibold text-gray-900"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Card Title&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Card description goes here.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"self-start px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Action
    &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The recipe for success
&lt;/h3&gt;

&lt;p&gt;The utility-first idea was not new. Frameworks like Tachyons and BassCSS had already explored atomic CSS, where every class sets exactly one property. But Tailwind was the first to package it with a thoughtful design system, excellent documentation, and a JIT compiler that made the developer experience genuinely fast. It became the standard not because the idea was new, but because the execution was unmatched.&lt;/p&gt;

&lt;p&gt;I genuinely love what the Tailwind team has built. The consistency it brings to projects is undeniable. The fact that you can look at any Tailwind component and immediately understand its layout is a huge productivity gain. But I also think we need to be honest about what it is and is not.&lt;/p&gt;

&lt;p&gt;It is a convenience layer over CSS. Every utility class maps to one or more CSS properties. You cannot do anything in Tailwind that you cannot also do in CSS. When CSS adds a feature that Tailwind has not generated a utility for, say &lt;code&gt;@container&lt;/code&gt; queries when they first landed, you drop down to arbitrary values or inline styles.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"[container-type:inline-size]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"[@container(min-width:400px)]:grid-cols-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The syntax also is not CSS. Tailwind ships with a complete baked-in design system. The spacing scale, the color palette, the breakpoints, the font sizes. You can customize all of it, and many teams do. But most developers never touch the config. They use the defaults. And those defaults are someone else's design decisions, inherited without thought.&lt;/p&gt;

&lt;p&gt;For me personally, I have found myself reaching for a different approach. UnoCSS is an on-demand atomic CSS engine that takes the utility-first idea and makes it unopinionated. It does not ship with a fixed set of utilities. Instead, it provides presets that you can mix, including one that emulates Tailwind exactly, and lets you build your own system. You define your color palette, your spacing scale, your breakpoints. Truly yours. It is the difference between renting an apartment and building your own house.&lt;/p&gt;

&lt;h3&gt;
  
  
  Broader influence
&lt;/h3&gt;

&lt;p&gt;The utility-first approach has been so influential that even traditional frameworks have adopted it. Bootstrap, which started as a component library with &lt;code&gt;.btn&lt;/code&gt; and &lt;code&gt;.card&lt;/code&gt; classes, now ships extensive utility classes in its recent versions. The industry has accepted that utilities are useful. The question is not whether to use them, but how much of a system you want to bring along with them.&lt;/p&gt;

&lt;p&gt;What is interesting is that utility engines depend on the same build tool infrastructure that made preprocessors universal. They scan your source files, detect which classes you use, and generate only the CSS you need. A modern build pipeline with Vite, PostCSS, and the Tailwind JIT compiler is doing more analysis and transformation than a Sass compilation ever did. The build step has become the foundation of how we use CSS, even when we think we are not using an abstraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where modern CSS takes us
&lt;/h2&gt;

&lt;p&gt;Walk through the features CSS has gained over the last few years and a pattern emerges. Nearly every major addition closes a gap that previously required an abstraction on top of CSS. The platform is not adding random features. It is systematically replacing the workarounds we built.&lt;/p&gt;

&lt;p&gt;Custom properties replaced the variables that Sass provided. Native nesting replaced Sass nesting. &lt;code&gt;@scope&lt;/code&gt; replaced the scoping that CSS Modules and styled-components handled. Cascade layers replaced the specificity management that naming conventions tried to manage. Container queries replaced responsive utility classes and JavaScript-based resize detection. &lt;code&gt;color-mix()&lt;/code&gt;, &lt;code&gt;oklch&lt;/code&gt;, and &lt;code&gt;light-dark()&lt;/code&gt; replaced color manipulation that previously required Sass functions or JavaScript color libraries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;--surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--text-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1a1a1a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#3498db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--surface&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="err"&gt;&amp;amp;&lt;/span&gt; &lt;span class="err"&gt;.card-header&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--text-primary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;400px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200px&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;utilities&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.surface-dim&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;--surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f5f5f5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cascade layers
&lt;/h3&gt;

&lt;p&gt;Cascade layers deserve special attention because they solve a problem that caused endless frustration. For years, managing specificity meant writing stronger selectors, reaching for &lt;code&gt;!important&lt;/code&gt;, or relying on naming conventions to keep styles from conflicting. Cascade layers give you explicit control. Define &lt;code&gt;base&lt;/code&gt;, &lt;code&gt;components&lt;/code&gt;, &lt;code&gt;utilities&lt;/code&gt;, and whatever layer comes last wins, regardless of specificity. It does not eliminate the cascade. It gives you a way to organize it that matches how you actually think about your styles. Every preprocessor and CSS-in-JS library built its own version of this. Now it is native.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@scope&lt;/code&gt; is something I have wanted for years. It lets you define a boundary for your styles without BEM naming or shadow DOM encapsulation. This is exactly what CSS Modules and styled-components were doing, just built into the language.&lt;/p&gt;

&lt;p&gt;Container queries changed how I think about responsive design. Instead of tying layout to the viewport, components can respond to their own container size. This matches how component-based architectures actually work, and it removes one of the main reasons teams reached for utility-based responsive classes.&lt;/p&gt;

&lt;p&gt;Subgrid, anchor positioning, and view transitions continue this pattern. Each one replaces something developers previously handled with JavaScript or awkward CSS workarounds. Individually, they are quality-of-life improvements. Taken together, they show a platform that has finally started closing the gaps.&lt;/p&gt;

&lt;p&gt;None of this means you should stop using your current tools. Tailwind is faster to write than raw CSS for most layouts. Styled-components solved real problems with scoping and co-location that CSS is only now catching up on. And there are massive codebases running Sass in production that will not be rewritten. They should not be. The investment in those tools is already made, and they work well.&lt;/p&gt;

&lt;p&gt;However, the question is no longer which abstraction to choose. The frontend community spent nearly twenty years building workarounds that browser vendors have slowly integrated into the platform itself. We built bridges, and then the landscape shifted so they were no longer needed. Not because the bridges were bad, but because the other side moved closer.&lt;/p&gt;

&lt;p&gt;CSS was never broken. It was just incomplete. Now it is finishing its sentences.&lt;/p&gt;

</description>
      <category>css</category>
      <category>webdev</category>
      <category>programming</category>
      <category>scss</category>
    </item>
    <item>
      <title>A tiny engine for generating file trees</title>
      <dc:creator>Ilyès</dc:creator>
      <pubDate>Sat, 18 Jul 2026 18:41:18 +0000</pubDate>
      <link>https://dev.to/hdzilyes/a-tiny-engine-for-generating-file-trees-4m</link>
      <guid>https://dev.to/hdzilyes/a-tiny-engine-for-generating-file-trees-4m</guid>
      <description>&lt;p&gt;I just tagged 1.0.0 of ts-treegen, a small TypeScript library for describing file structures as data and writing them to disk.&lt;/p&gt;

&lt;p&gt;If you've ever built a CLI, a scaffolding tool, or anything that needs to generate a bunch of files and folders, you know the usual approach: a pile of &lt;code&gt;fs.writeFileSync&lt;/code&gt; calls, manual path joins, and conditional logic scattered everywhere. ts-treegen is my attempt at making that feel less like plumbing and more like just describing what you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ts-treegen/node&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;README.md&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;# My New App&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;console.log('hello');&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;targetDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./output&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;file()&lt;/code&gt; and &lt;code&gt;dir()&lt;/code&gt; build a tree. &lt;code&gt;emit()&lt;/code&gt; resolves it. &lt;code&gt;plan()&lt;/code&gt; figures out what needs to be written and gives you a chance to inspect it before anything touches disk. That's the whole API.&lt;/p&gt;

&lt;p&gt;Conditional files don't need any special syntax either. It's just JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;isProd&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.env.production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NODE_ENV=production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No template tags, no wrapper nodes to learn. If a value is falsy, it's filtered out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it this way
&lt;/h2&gt;

&lt;p&gt;The goal from the start was to keep the surface area small enough that you could hold the whole API in your head after reading the README once. I went through a few iterations before landing here, and each one was mostly about removing things rather than adding them. Conflict resolution collapsed down to a single &lt;code&gt;overwrite&lt;/code&gt; flag. Copy helpers got cut because &lt;code&gt;fs.cp&lt;/code&gt; already does the job. Custom error types got replaced with things you'd actually reach for in normal code. Every feature I kept had to earn its place by solving something real, not just being possible to build.&lt;/p&gt;

&lt;p&gt;Along the way the library also became runtime-agnostic. The core has zero dependencies and works against a small &lt;code&gt;FileSystem&lt;/code&gt; interface, so I/O is fully pluggable. &lt;code&gt;ts-treegen/node&lt;/code&gt; wires up Node's &lt;code&gt;fs/promises&lt;/code&gt; for you out of the box, and there are equivalents for Deno, Bun, Cloudflare Workers, and an in-memory implementation for testing. Implementing your own is easy too, since the interface only has four methods.&lt;/p&gt;

&lt;p&gt;Under the hood there's also fail-fast path safety: directory traversal and absolute path escapes get caught before anything is written, so a bad path in your data can't silently write somewhere it shouldn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it landed
&lt;/h2&gt;

&lt;p&gt;Getting to 1.0 mostly meant settling on names and shapes I was confident wouldn't need to change again. &lt;code&gt;access()&lt;/code&gt; became &lt;code&gt;exists()&lt;/code&gt; because it better describes what the method actually does, and it now returns a boolean instead of throwing. &lt;code&gt;write()&lt;/code&gt; became &lt;code&gt;plan()&lt;/code&gt;, giving you a proper object you can inspect and run rather than a fire-and-forget call. Small changes, but they're the kind you want to get right before people start depending on them.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://ilyeshdz.github.io/personal/posts/a-tiny-engine-for-generating-file-trees/#try-it" rel="noopener noreferrer"&gt;Try it&lt;/a&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;ts-treegen

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's zero-dependency, tiny, and works anywhere JavaScript runs. Docs and source are on &lt;a href="https://github.com/ilyeshdz/ts-treegen" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. I'd love to hear what you build with it.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>cli</category>
    </item>
    <item>
      <title>Just use Deno</title>
      <dc:creator>Ilyès</dc:creator>
      <pubDate>Sat, 18 Jul 2026 17:35:28 +0000</pubDate>
      <link>https://dev.to/hdzilyes/just-use-deno-2ai9</link>
      <guid>https://dev.to/hdzilyes/just-use-deno-2ai9</guid>
      <description>&lt;p&gt;Every time I start a new project, there's a moment where I open my terminal and type &lt;code&gt;npm init&lt;/code&gt;. Not because I thought about it. Just because that's what you do. Node.js is the water JavaScript developers swim in, and most of us never stop to notice we're wet.&lt;/p&gt;

&lt;p&gt;My answer, at least for new projects, is no. &lt;strong&gt;Just use Deno.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Node years
&lt;/h2&gt;

&lt;p&gt;I started with &lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;Node&lt;/a&gt; like almost everyone else. It was everywhere, the community was massive, and every problem I ran into had three Stack Overflow answers and a Medium post from 2017 to go with it. I got good at it, not just "can build a REST API" good, but the kind of comfortable where you stop thinking about the tool and just think about the problem.&lt;/p&gt;

&lt;p&gt;However, somewhere along the way, that comfort started to feel expensive.&lt;/p&gt;

&lt;p&gt;I'm not talking about the runtime itself. What I mean is the &lt;em&gt;ecosystem overhead&lt;/em&gt;. The moment you start a real project, you're not just writing code. You're picking a linter, configuring ESLint, wiring Prettier, writing a &lt;code&gt;tsconfig.json&lt;/code&gt;, deciding on a test runner, installing &lt;code&gt;ts-node&lt;/code&gt; just to run a TypeScript file. Before you've written a single line of business logic, you've made fifteen decisions and touched seven config files.&lt;/p&gt;

&lt;p&gt;For a while I assumed this was just the cost of doing things properly. Then I started to wonder if it was just the cost of Node.&lt;/p&gt;

&lt;p&gt;The existence of &lt;code&gt;create-next-app&lt;/code&gt;, &lt;code&gt;create-remix&lt;/code&gt;, &lt;code&gt;create-t3-app&lt;/code&gt; and the rest of the &lt;code&gt;create-*&lt;/code&gt; CLIs is basically a confession. The stack got so heavy, so &lt;em&gt;fixed&lt;/em&gt;, that the community built scaffolding tools just to make it bearable to start. That's not a feature. That's a workaround for a problem that became so normalized nobody questions it anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://ilyeshdz.github.io/personal/posts/just-use-deno/#first-look-at-deno-and-why-i-dismissed-it" rel="noopener noreferrer"&gt;First look at Deno, and why I dismissed it&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;I ran into &lt;a href="https://deno.com/" rel="noopener noreferrer"&gt;Deno&lt;/a&gt; in 2021. Ryan Dahl's &lt;a href="https://www.youtube.com/watch?v=M3BM9TB-8yA" rel="noopener noreferrer"&gt;"10 things I regret about Node.js"&lt;/a&gt; talk had been making the rounds, and Deno felt like the answer he was building toward: TypeScript baked in, a proper security model, no &lt;code&gt;node_modules&lt;/code&gt; in sight.&lt;/p&gt;

&lt;p&gt;It was interesting. It was also not ready for me.&lt;/p&gt;

&lt;p&gt;The ecosystem was small in the "the package I need doesn't exist yet" sense. The permission flags felt like friction. And the URL-based imports made me uneasy in a way I couldn't fully articulate at first. It's the same feeling I got with Go's module system early on: something about pulling a random URL as a dependency just felt &lt;em&gt;off&lt;/em&gt;. What happens when that URL disappears? Who's auditing this? It had a security smell I didn't trust yet.&lt;/p&gt;

&lt;p&gt;I filed it under "promising, check back later" and went back to Node.&lt;/p&gt;

&lt;h2&gt;
  
  
  The turning point
&lt;/h2&gt;

&lt;p&gt;What changed wasn't Deno. What changed was how tired I got.&lt;/p&gt;

&lt;p&gt;Not from a single bad project, but from the accumulation. The fifteenth time I copy-pasted a &lt;code&gt;tsconfig.json&lt;/code&gt;. The third time in a week I spent twenty minutes debugging a module resolution error that had nothing to do with my actual code.&lt;/p&gt;

&lt;p&gt;So I went back to Deno. This time with different eyes.&lt;/p&gt;

&lt;p&gt;The thing that hit me wasn't a feature. It was an &lt;em&gt;absence&lt;/em&gt;. I cloned nothing. I installed nothing. I ran a TypeScript file with &lt;code&gt;deno run&lt;/code&gt; and it just worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;deno run hello.ts
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;No build step. No config. Just runs.
&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The formatter, linter, and test runner were already there, as quiet defaults I could override if I wanted. And the security model, which had annoyed me before, suddenly made sense. When you run &lt;code&gt;deno run --allow-net script.ts&lt;/code&gt;, you know exactly what that script can do. That's not a nuisance. That's a professional safeguard. With the task system in &lt;code&gt;deno.json&lt;/code&gt;, you define your permissions once and stop thinking about them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;deno.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tasks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deno run --allow-net --watch main.ts"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hono"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm:hono"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scripts, internal tools, lightweight APIs felt noticeably faster to build. Not because Deno is magic, but because I was spending more time on the actual problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest tension
&lt;/h2&gt;

&lt;p&gt;Deno originally aimed to be genuinely different, not just a better Node but a different philosophy. URL-based imports, web-standard APIs, a security model that assumed nothing. The vision was coherent. And also, honestly, the URL imports were part of why I stayed skeptical for so long. Pulling a dependency from a raw URL felt weird, same vibe I got from Go's early module system. A dependency that's just a URL lives and dies with that URL. Not a small concern.&lt;/p&gt;

&lt;p&gt;Then reality hit. The Node ecosystem is enormous, load-bearing for the internet kind of enormous. For Deno to be usable, it had to support npm packages and Node APIs. Throughout 2023, &lt;a href="https://docs.deno.com/runtime/fundamentals/node/" rel="noopener noreferrer"&gt;that's exactly what happened&lt;/a&gt;: npm-style imports landed properly, Node built-ins like &lt;code&gt;node:fs&lt;/code&gt; became supported, and &lt;code&gt;package.json&lt;/code&gt; interop started working. That's the moment the URL anxiety became mostly moot, you could just use &lt;code&gt;npm:&lt;/code&gt; specifiers like a normal person.&lt;/p&gt;

&lt;p&gt;Then in March 2024, they announced &lt;a href="https://jsr.io/" rel="noopener noreferrer"&gt;JSR&lt;/a&gt; in public beta. A TypeScript-first registry, works with Node and Bun too, no build step to publish. It's a superset of npm, not a replacement, so it plays nice with everything you already use. That one-two punch from 2023 to early 2024 is honestly what made Deno feel like something you could actually bet on.&lt;/p&gt;

&lt;p&gt;I don't think the original tension resolves cleanly. The more Node-compatible Deno becomes, the more it risks just being Node with better defaults. But what it &lt;em&gt;can&lt;/em&gt; be, and increasingly is, is a better place to build &lt;em&gt;new&lt;/em&gt; things, even if it never displaces the old ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why enterprises won't move
&lt;/h2&gt;

&lt;p&gt;If Deno is so good, why isn't everyone using it?&lt;/p&gt;

&lt;p&gt;Because migration is expensive and organizations are rational. If you're running a production Node.js service with six years of history and a team of twenty, switching runtimes isn't a technical decision, it's a risk management one. Auditing compatibility, retraining the team, updating CI/CD, defending the call when something breaks. Hard sell for a marginal gain.&lt;/p&gt;

&lt;p&gt;Ecosystem inertia isn't stupidity. It's survival instinct at scale.&lt;/p&gt;

&lt;p&gt;However, that argument only applies to existing projects. For something new, a greenfield API, a CLI tool, a prototype, there's no migration cost. There's only a choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  So just use Deno
&lt;/h2&gt;

&lt;p&gt;For scripts, tools, prototypes, and small-to-medium APIs, especially in TypeScript, Deno gives you a cleaner start. Integrated toolchain, secure-by-default, a modern registry in &lt;a href="https://jsr.io/" rel="noopener noreferrer"&gt;JSR&lt;/a&gt; that takes TypeScript seriously, and zero setup before your first function.&lt;/p&gt;

&lt;p&gt;And if you need npm packages? You can. Deno's compatibility is good enough now that it doesn't force a binary choice anymore.&lt;/p&gt;

&lt;p&gt;Deno is not perfect. It may never fully replace Node, and that's fine. But defaulting to Node for new projects is increasingly a habit rather than a decision.&lt;/p&gt;

&lt;p&gt;Break the habit. &lt;strong&gt;&lt;a href="https://deno.com/" rel="noopener noreferrer"&gt;Just use Deno.&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>javascriptruntime</category>
    </item>
    <item>
      <title>Let's talk about Deno Desktop</title>
      <dc:creator>Ilyès</dc:creator>
      <pubDate>Fri, 17 Jul 2026 16:50:41 +0000</pubDate>
      <link>https://dev.to/hdzilyes/lets-talk-about-deno-desktop-2i6p</link>
      <guid>https://dev.to/hdzilyes/lets-talk-about-deno-desktop-2i6p</guid>
      <description>&lt;p&gt;I've been building web applications for a long time. But there's one thing I've always wanted to build: a desktop application.&lt;/p&gt;

&lt;p&gt;There are plenty of solutions out there that let you build desktop apps with web technologies. Some of them use completely different languages for the backend, which never felt right to me. Others, like Electron, use Node.js with Chromium or the host's native web engine. They're genuinely impressive.&lt;/p&gt;

&lt;p&gt;However, they wouldn't use Deno.&lt;/p&gt;

&lt;p&gt;Deno is built to be minimal. And there was no minimal solution based on Deno that provided the same capabilities as Electron, in a simple way, mostly compatible with most Deno projects.&lt;/p&gt;

&lt;p&gt;I've always wanted an Electron-like but for the Deno landscape. When 2.9 shipped, it was finally there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every desktop framework asks you to learn a second project
&lt;/h2&gt;

&lt;p&gt;This is what frustrates me about desktop web frameworks.&lt;/p&gt;

&lt;p&gt;Electron wants a &lt;code&gt;main.js&lt;/code&gt; and a &lt;code&gt;renderer.js&lt;/code&gt; and a preload script and a build step before you get a single pixel. Tauri wants a whole Rust project living alongside your frontend. You have to learn a second project structure just to open a native window.&lt;/p&gt;

&lt;p&gt;Deno Desktop makes a different bet. You already know how to serve HTML.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;!DOCTYPE html&amp;gt;&amp;lt;h1&amp;gt;Hello from desktop&amp;lt;/h1&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deno desktop main.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. The runtime picks a local port. Your handler binds to it automatically. A window opens. No port management, no extra config, no learning a second mental model.&lt;/p&gt;

&lt;p&gt;The reason I like this is simple: the best API is the one you don't have to learn. That sounds obvious, but almost every desktop framework violates it.&lt;/p&gt;

&lt;h2&gt;
  
  
  You get to choose your engine, and the default is the right one
&lt;/h2&gt;

&lt;p&gt;What I like is that Deno Desktop supports both approaches. You can use your OS native web engine, or you can bundle Chromium if you need pixel-perfect consistency everywhere. The default is the OS engine.&lt;/p&gt;

&lt;p&gt;That default tells you a lot about their thinking. WebView keeps your app around 66 MB uncompressed, or 19 MB with &lt;code&gt;--compress&lt;/code&gt;. CEF bumps that to roughly 150 MB. By making webview the default and CEF opt-in, Deno is saying: most apps don't need Chromium's rendering fidelity, and the people who do will know it. 19 MB versus 150 MB is not a small difference.&lt;/p&gt;

&lt;p&gt;There's also a raw mode underneath that removes the browser engine entirely, meant for WebGPU or custom rendering. I don't think it's useful yet. But maybe someday someone will build a Deno library on top of it that lets you draw directly to the screen with no web engine at all. That would be fun. I haven't heard of anyone doing it yet.&lt;/p&gt;

&lt;p&gt;However, the flexibility itself is what matters. And the binary size argument isn't even the thing I find most interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  In-process channels are my favorite part
&lt;/h2&gt;

&lt;p&gt;Here's the thing nobody talks about with desktop frameworks: once your page is on screen, it still needs to talk to the backend. Most frameworks send messages across processes to do that. It works, but you feel it in startup sequences that query the backend for initial state. Those round trips add up.&lt;/p&gt;

&lt;p&gt;Deno Desktop keeps everything closer. The runtime and the renderer don't cross process boundaries just to talk to each other.&lt;/p&gt;

&lt;p&gt;You register a function on the Deno side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BrowserWindow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;readSettings&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readTextFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;settings.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And call it from the webview:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bindings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readSettings&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No message channels, no serialization ceremony. If you've used Electron, the mapping is straightforward: &lt;code&gt;ipcMain.handle&lt;/code&gt; becomes &lt;code&gt;win.bind&lt;/code&gt;, &lt;code&gt;ipcRenderer.invoke&lt;/code&gt; becomes &lt;code&gt;bindings.name()&lt;/code&gt;, and &lt;code&gt;contextBridge&lt;/code&gt; just disappears.&lt;/p&gt;

&lt;p&gt;Why do I care? Because this removes friction you don't notice until it's gone. Each individual call is fast enough in any framework. But they accumulate. The app starts feeling slightly heavy, slightly remote. You can't point at why. In-process calls fix that. Not a revolution, just removing something that shouldn't have been there.&lt;/p&gt;

&lt;h2&gt;
  
  
  This philosophy is the whole story
&lt;/h2&gt;

&lt;p&gt;What I keep coming back to is that none of these decisions are random.&lt;/p&gt;

&lt;p&gt;Deno already has everything needed to run and package your code. Desktop uses those pieces instead of inventing a second system. The same code that powers your server powers your desktop app. No extra build step.&lt;/p&gt;

&lt;p&gt;That consistency is what makes it feel like Deno. Batteries included, zero config, escape hatches when you need them.&lt;/p&gt;

&lt;p&gt;Every choice in Deno Desktop follows from that same belief:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Default to webview, not Chromium.&lt;/li&gt;
&lt;li&gt;Ship in-process channels instead of IPC.&lt;/li&gt;
&lt;li&gt;Auto-detect your framework instead of asking what you're using.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't isolated technical optimizations. They're all the same thesis: the default path should be the right path, and configuration should be opt-in for people who actually need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd honestly tell someone considering it
&lt;/h2&gt;

&lt;p&gt;I should be straight about what's missing. Windows auto-update isn't implemented yet. Notarization is a separate manual step. Mobile support doesn't exist.&lt;/p&gt;

&lt;p&gt;I'm not sure all of those are blockers for most projects. The foundation is what matters. Default webview, in-process channels, single command output. The value of a desktop framework is making the easy path the correct one, and this does that.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;deno desktop&lt;/code&gt; is the first desktop framework that made me think I could reach for it today. Not when the ecosystem fills in, not after another rewrite cycle. Today. I think that's worth saying out loud.&lt;/p&gt;

</description>
      <category>deno</category>
      <category>javascript</category>
      <category>programming</category>
      <category>news</category>
    </item>
  </channel>
</rss>
