<?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: Masood Abdolian</title>
    <description>The latest articles on DEV Community by Masood Abdolian (@abdolian).</description>
    <link>https://dev.to/abdolian</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%2F1079593%2Fc71f6022-1c85-4b5b-8fce-6dd7da85091d.png</url>
      <title>DEV Community: Masood Abdolian</title>
      <link>https://dev.to/abdolian</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abdolian"/>
    <language>en</language>
    <item>
      <title>I built a compiler so I could stop writing custom element boilerplate</title>
      <dc:creator>Masood Abdolian</dc:creator>
      <pubDate>Mon, 07 Sep 2026 06:32:29 +0000</pubDate>
      <link>https://dev.to/abdolian/i-built-a-compiler-so-i-could-stop-writing-custom-element-boilerplate-383h</link>
      <guid>https://dev.to/abdolian/i-built-a-compiler-so-i-could-stop-writing-custom-element-boilerplate-383h</guid>
      <description>&lt;p&gt;I've been building &lt;a href="https://github.com/htmlplus/element" rel="noopener noreferrer"&gt;HTMLPlus Element&lt;/a&gt; on and off for a few years. It's at v4 now, 500+ commits, and I've never really shown it to anyone. So this is me finally doing that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I got tired of
&lt;/h2&gt;

&lt;p&gt;Custom elements are a good idea. The browser gives you a component model that outlives whichever framework you happen to be using this year. What it does not give you is a pleasant way to write one.&lt;/p&gt;

&lt;p&gt;Every component ends up carrying the same freight: a &lt;code&gt;customElements.define&lt;/code&gt; call, a static block declaring which properties are observed, attribute-to-property conversion written by hand for anything that isn't a string, a place to hang your styles, and — if anyone else is going to use your elements — a set of TypeScript definitions you maintain separately from the code they describe.&lt;/p&gt;

&lt;p&gt;None of that is the component. It's the tax you pay to have one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did instead
&lt;/h2&gt;

&lt;p&gt;I moved that work into a compiler. You write the class, and the build step figures out the rest.&lt;/p&gt;

&lt;p&gt;Here is a complete component:&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;Element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;State&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;@htmlplus/element&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="nd"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyCounter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;State&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;render&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;host&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Count is &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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;host&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole file, &lt;code&gt;my-counter.tsx&lt;/code&gt;. A few things happened without me writing them:&lt;/p&gt;

&lt;p&gt;The tag name came from the class name. &lt;code&gt;MyCounter&lt;/code&gt; becomes &lt;code&gt;my-counter&lt;/code&gt;, so the name lives in one place instead of two.&lt;/p&gt;

&lt;p&gt;The styles came from &lt;code&gt;my-counter.css&lt;/code&gt; sitting next to it. Same base name, picked up automatically — no import, no styles array, no template literal.&lt;/p&gt;

&lt;p&gt;Property types were resolved from the TypeScript types you already wrote. A &lt;code&gt;number&lt;/code&gt; property arrives as a number when someone sets the attribute in HTML, without a converter.&lt;/p&gt;

&lt;p&gt;And the TypeScript definitions your consumers need were generated as part of the build, from the same source, so they can't drift out of sync with the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else is in there
&lt;/h2&gt;

&lt;p&gt;Everything you'd reach for is a decorator in the same package, so there's nothing extra to install: properties, state, events, listeners, public methods, watchers, shadow DOM queries, slots, and context via &lt;code&gt;@Provider&lt;/code&gt; and &lt;code&gt;@Consumer&lt;/code&gt;. There are small helpers too — &lt;code&gt;@Bind&lt;/code&gt;, &lt;code&gt;@Debounce&lt;/code&gt;, &lt;code&gt;@Direction&lt;/code&gt; for RTL/LTR.&lt;/p&gt;

&lt;p&gt;The output works in any framework as it is. No wrapper packages to generate and keep in step: events are emitted under the naming convention each framework expects, so React, Vue, Angular, Svelte, Solid, Qwik and others consume the same element without a shim.&lt;/p&gt;

&lt;p&gt;The runtime is about 8 KB gzipped for a typical element, and under 11 KB for the whole library.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not theoretical
&lt;/h2&gt;

&lt;p&gt;I use it to build &lt;a href="https://www.htmlplus.io/" rel="noopener noreferrer"&gt;htmlplus.io&lt;/a&gt;, a UI kit. Those components are written exactly the way the counter above is, and the examples on that site run in Angular, React, Vue and Svelte. If you'd rather look at output than at API design, start there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it's the wrong tool
&lt;/h2&gt;

&lt;p&gt;It compiles, which means a build step. If you want to drop a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag on a page and start writing an element, or you need two components and not twenty, a runtime-only library is a better fit and I'd rather say so than have you find out later.&lt;/p&gt;

&lt;p&gt;It's a bundler plugin rather than a toolchain of its own — Vite and Rollup today, built on unplugin, so more can follow. Server-side rendering to Declarative Shadow DOM is in progress but not released.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm init @htmlplus/element@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/htmlplus/element" rel="noopener noreferrer"&gt;https://github.com/htmlplus/element&lt;/a&gt; (MIT)&lt;/p&gt;

&lt;p&gt;I'd rather have criticism of the API design than stars. If something here looks wrong to you, I genuinely want to hear it.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>opensource</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
