<?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: Andrzej Stacherski</title>
    <description>The latest articles on DEV Community by Andrzej Stacherski (@andrzej_stacherski).</description>
    <link>https://dev.to/andrzej_stacherski</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%2F4239092%2F8cca02d5-691f-4b44-bd19-2fec13080e34.png</url>
      <title>DEV Community: Andrzej Stacherski</title>
      <link>https://dev.to/andrzej_stacherski</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/andrzej_stacherski"/>
    <language>en</language>
    <item>
      <title>A web component without Shadow DOM: build one with plain JavaScript</title>
      <dc:creator>Andrzej Stacherski</dc:creator>
      <pubDate>Wed, 11 Nov 2026 13:00:27 +0000</pubDate>
      <link>https://dev.to/andrzej_stacherski/a-web-component-without-shadow-dom-build-one-with-plain-javascript-14e4</link>
      <guid>https://dev.to/andrzej_stacherski/a-web-component-without-shadow-dom-build-one-with-plain-javascript-14e4</guid>
      <description>&lt;p&gt;&lt;strong&gt;Build a custom element with plain JavaScript and plain CSS: lifecycle, properties vs attributes, and the two traps that break most first components.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most web component tutorials open with Shadow DOM, templates and slots. You can skip all of that and still get a useful, reusable component. Here is a card element in vanilla JavaScript. It works with your normal CSS and needs no build step.&lt;/p&gt;

&lt;p&gt;The goal is to turn this:&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'article'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'card'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...a dozen lines of createElement and append&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into this:&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ml-card'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fact&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&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 in HTML, you can write &lt;code&gt;&amp;lt;ml-card hint&amp;gt;&amp;lt;/ml-card&amp;gt;&lt;/code&gt; as if browsers had always shipped a card tag.&lt;/p&gt;

&lt;p&gt;Two details in that tag are worth explaining up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ml-&lt;/code&gt;&lt;/strong&gt; is a prefix I use on every component. It stands for &lt;strong&gt;Microlearn&lt;/strong&gt;, the name of the app I'm building. The prefix is what keeps my tags from clashing with other people's code or with future HTML elements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hint&lt;/code&gt;&lt;/strong&gt; is a boolean attribute I invented for this card. It switches on a small "Scroll for the next one ↓" line at the bottom of the card. It works like the built-in &lt;code&gt;disabled&lt;/code&gt; or &lt;code&gt;checked&lt;/code&gt;: if the attribute is present, the hint shows; if it's absent, the hint stays hidden. I put it only on the first card of the feed, so new users learn they can scroll, and the rest of the cards stay clean. We'll see how the component reads it further down.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The smallest custom element
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements" rel="noopener noreferrer"&gt;custom element&lt;/a&gt; is a class that extends &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement" rel="noopener noreferrer"&gt;&lt;code&gt;HTMLElement&lt;/code&gt;&lt;/a&gt;, registered under a tag name:&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MlCard&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;HTMLElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;connectedCallback&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="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Hello from a card'&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="n"&gt;customElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ml-card'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;MlCard&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;Three things to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The tag name must contain a hyphen.&lt;/strong&gt; &lt;code&gt;ml-card&lt;/code&gt; works, &lt;code&gt;card&lt;/code&gt; does not. The hyphen is how the browser tells your tags from its own, now and in the future. That's why I start every name with &lt;code&gt;ml-&lt;/code&gt; (Microlearn).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;connectedCallback&lt;/code&gt;&lt;/strong&gt; is called by the browser when the element is added to the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define" rel="noopener noreferrer"&gt;&lt;code&gt;customElements.define&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; registers the tag. Every &lt;code&gt;&amp;lt;ml-card&amp;gt;&lt;/code&gt;, existing or added later, becomes an instance of your class.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Build once, render many times
&lt;/h2&gt;

&lt;p&gt;Here is the real component:&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MlCard&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;HTMLElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;observedAttributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'hint'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  #&lt;span class="n"&gt;fact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// the fact to show: { id, t, f, m }&lt;/span&gt;
  #&lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// the topic it belongs to: { label, hue }&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&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;super&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="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;connectedCallback&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;#&lt;span class="nf"&gt;build&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="kc"&gt;this&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="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;attributeChangedCallback&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kc"&gt;this&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="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="nf"&gt;fact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;#&lt;span class="n"&gt;fact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kc"&gt;this&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="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;fact&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;return&lt;/span&gt; &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;#&lt;span class="n"&gt;fact&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="nf"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;#&lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kc"&gt;this&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="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  #&lt;span class="nf"&gt;build&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="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`
      &amp;lt;p class="ml-card__topic"&amp;gt;&amp;lt;/p&amp;gt;
      &amp;lt;h2 class="ml-card__fact"&amp;gt;&amp;lt;/h2&amp;gt;
      &amp;lt;p class="ml-card__more"&amp;gt;&amp;lt;/p&amp;gt;
      &amp;lt;p class="ml-card__hint" hidden&amp;gt;Scroll for the next one ↓&amp;lt;/p&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&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="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Guard: stop if the markup isn't built yet, or the data hasn't arrived.&lt;/span&gt;
    &lt;span class="c1"&gt;// This is what makes it safe to call #render at any moment.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ready&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;#&lt;span class="n"&gt;fact&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;#&lt;span class="n"&gt;topic&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="c1"&gt;// Set a CSS custom property (--h) on the card, so the stylesheet can&lt;/span&gt;
    &lt;span class="c1"&gt;// use the topic's hue for colours (e.g. hsl(var(--h) 70% 50%)).&lt;/span&gt;
    &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--h'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;#&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Fill in the topic label, e.g. "Space". textContent inserts plain&lt;/span&gt;
    &lt;span class="c1"&gt;// text, never HTML, so the data can't inject markup.&lt;/span&gt;
    &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.ml-card__topic'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;#&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Fill in the headline: the fact itself (field "f").&lt;/span&gt;
    &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.ml-card__fact'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;#&lt;span class="n"&gt;fact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Fill in the extra detail under the headline (field "m", for "more").&lt;/span&gt;
    &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.ml-card__more'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;#&lt;span class="n"&gt;fact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Show the hint only if the card has the `hint` attribute.&lt;/span&gt;
    &lt;span class="c1"&gt;// hasAttribute returns true/false; "hidden" is the opposite, hence the !.&lt;/span&gt;
    &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.ml-card__hint'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hint'&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ml-card'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ml-card'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;MlCard&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;The same shape works for any component:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The constructor only sets defaults.&lt;/strong&gt; Don't read attributes or touch children there. The element may not be in the page yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;connectedCallback&lt;/code&gt; builds the markup once.&lt;/strong&gt; The &lt;code&gt;ready&lt;/code&gt; flag matters: the callback runs again every time the element is moved or reconnected, and without the flag you would build twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;#render&lt;/code&gt; is safe to call at any moment.&lt;/strong&gt; It returns early if there is nothing to draw yet, and calling it five times gives the same result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data is poured in with &lt;code&gt;textContent&lt;/code&gt;&lt;/strong&gt;, never placed inside the &lt;code&gt;innerHTML&lt;/code&gt; string. The template is fixed text you wrote, so it is safe.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;#&lt;/code&gt; names are &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_elements" rel="noopener noreferrer"&gt;private class members&lt;/a&gt;. Only code inside the class can use them, which keeps the public surface small: &lt;code&gt;fact&lt;/code&gt;, &lt;code&gt;topic&lt;/code&gt; and the &lt;code&gt;hint&lt;/code&gt; attribute.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are &lt;code&gt;#fact&lt;/code&gt; and &lt;code&gt;#topic&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;They are the card's two pieces of data, stored in private fields. Each starts as &lt;code&gt;null&lt;/code&gt; ("nothing yet") and is filled in from outside.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;#fact&lt;/code&gt;&lt;/strong&gt; is one entry from the app's list of facts, an object with four keys:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'b0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'space'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'A day on Venus is longer than its year.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Venus turns once every 243 Earth days...'&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;id&lt;/code&gt; identifies the fact, &lt;code&gt;t&lt;/code&gt; is its topic key, &lt;code&gt;f&lt;/code&gt; is the headline and &lt;code&gt;m&lt;/code&gt; is the longer explanation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;#topic&lt;/code&gt;&lt;/strong&gt; describes the topic that fact belongs to. It holds a display name and a colour:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Space'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;230&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app keeps a lookup table of topics, and the calling code picks the right one with &lt;code&gt;TOPICS[fact.t]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why two fields and not one? The fact says &lt;em&gt;what&lt;/em&gt; to show, and the topic says &lt;em&gt;how it looks&lt;/em&gt; (its label and colour). Many facts share one topic, so the topic is stored once and passed in separately.&lt;/p&gt;

&lt;p&gt;Why private? Because the outside world never touches the fields directly. It goes through the public setters, &lt;code&gt;card.fact = ...&lt;/code&gt; and &lt;code&gt;card.topic = ...&lt;/code&gt;, and each setter stores the value and calls &lt;code&gt;#render&lt;/code&gt;. That is how the card redraws itself whenever new data arrives. The &lt;code&gt;fact&lt;/code&gt; getter exists so other code can ask the card which fact it holds. There is no &lt;code&gt;topic&lt;/code&gt; getter because nothing needed one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Properties or attributes?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A flag or short text (&lt;code&gt;hint&lt;/code&gt;, &lt;code&gt;size="large"&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Attribute&lt;/td&gt;
&lt;td&gt;Visible in HTML, usable from a CMS, styleable with &lt;code&gt;[hint]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An object or array&lt;/td&gt;
&lt;td&gt;Property&lt;/td&gt;
&lt;td&gt;Attributes can only hold strings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;hint&lt;/code&gt; is the attribute case. Inside &lt;code&gt;#render&lt;/code&gt;, this line does the work:&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="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.ml-card__hint'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hint'&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;It checks whether the &lt;code&gt;hint&lt;/code&gt; attribute is present and shows or hides the hint paragraph to match. For the card to redraw when the attribute changes later, list it in &lt;code&gt;static observedAttributes&lt;/code&gt;. After that, &lt;code&gt;card.toggleAttribute('hint')&lt;/code&gt; updates the card on its own, with no extra JavaScript on your side. In the feed, I use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/toggleAttribute" rel="noopener noreferrer"&gt;&lt;code&gt;toggleAttribute('hint', index === 0)&lt;/code&gt;&lt;/a&gt; so only the first card gets it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: the blank card
&lt;/h2&gt;

&lt;p&gt;Look at the order of events in the calling code:&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ml-card'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fact&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// setter runs, but there is no markup yet&lt;/span&gt;
&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// same&lt;/span&gt;
&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// now connectedCallback builds the markup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both setters run before the card is in the page. The guard on the first line of &lt;code&gt;#render&lt;/code&gt; makes them harmless: they store the value and return. When &lt;code&gt;connectedCallback&lt;/code&gt; fires, &lt;code&gt;#render&lt;/code&gt; picks up what was stored. Without the guard you get blank cards or errors. If a card renders blank, check the order of events first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2: the collapsed card
&lt;/h2&gt;

&lt;p&gt;A custom element is &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/display" rel="noopener noreferrer"&gt;&lt;code&gt;display: inline&lt;/code&gt;&lt;/a&gt; by default, like a &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;, and inline elements ignore &lt;code&gt;height&lt;/code&gt;. Forget this and your cards shrink to the height of their text.&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="nt"&gt;ml-card&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;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100dvh&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;Whenever you register a tag, decide what &lt;code&gt;display&lt;/code&gt; it should have and write that rule first.&lt;/p&gt;

&lt;h2&gt;
  
  
  A related gotcha: define twice
&lt;/h2&gt;

&lt;p&gt;Calling &lt;code&gt;customElements.define&lt;/code&gt; twice for the same name throws. In your own page that never happens. In a CMS, where a theme and a module may both load the same script, it happens all the time. The &lt;code&gt;get&lt;/code&gt; check before &lt;code&gt;define&lt;/code&gt; makes loading the script twice harmless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why no Shadow DOM?
&lt;/h2&gt;

&lt;p&gt;An honest trade-off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you give up:&lt;/strong&gt; real style isolation. Another stylesheet could restyle your card by accident. The prefix (&lt;code&gt;ml-card__topic&lt;/code&gt;, not &lt;code&gt;.topic&lt;/code&gt;) is your only protection, so use it everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you gain:&lt;/strong&gt; your normal CSS, design tokens and dark mode just work. The inspector shows plain HTML. Components drop into a CMS and inherit the surrounding page's styles, which is often exactly what you want.&lt;/p&gt;

&lt;p&gt;For an app or design system you control, light DOM is the shorter road. Shadow DOM earns its effort when you ship a widget into pages you don't own.&lt;/p&gt;

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

&lt;p&gt;Add a &lt;code&gt;compact&lt;/code&gt; attribute that makes the headline smaller. You need one entry in &lt;code&gt;observedAttributes&lt;/code&gt; and one CSS rule: &lt;code&gt;ml-card[compact] .ml-card__fact&lt;/code&gt;. Notice that the attribute does its work through CSS alone.&lt;/p&gt;




&lt;p&gt;This component comes from a small book I'm writing, where you build a full "one fact per screen" learning app with vanilla JavaScript and web components, chapter by chapter. Chapter 1 is free to read: &lt;a href="https://onescreenatatime.netlify.app/book/chapter-1/" rel="noopener noreferrer"&gt;One Screen at a Time&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webcomponents</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
