Top
Best
New

Posted by todsacerdoti 12/29/2025

You can make up HTML tags(maurycyz.com)
584 points | 190 commentspage 2
ianbutler 12/29/2025|
https://lit.dev/

That's the premise behind Lit (using the custom elements api)! I've been using it to build out a project recently and it's quite good, simpler to reason about than the current state of React imo.

It started at google and now is part of OpenJS.

sleazebreeze 12/29/2025|
I just evaluated Lit for work and while we didn't go with it, it was very nice. I love the base custom elements API regardless of using Lit or not, turns out that's all you really need to make intricate UIs that feel seamless.
g0wda 12/29/2025|||
Something about Lit/web components that's not written clearly on the label is the hoops you have to jump through to style elements. If you're using tailwind in the whole app, you just want to pull it into your custom elements without much boilerplate. My main app compiles a single minified app.css, it felt not so modular to now include that in every single component. The alternative is create a subclass that injects tailwind into Lit components. It'd be perfect it just had a switch to inherit everything the page already has.
hunterloftis 12/29/2025|||
> It'd be perfect it just had a switch to inherit everything the page already has.

It does! <https://lit.dev/docs/components/shadow-dom/>

By default, Lit renders into shadow DOM. This carries benefits like encapsulation (including the style encapsulation you mention). If you prefer global styles, you can render into light DOM instead with that one-line switch.

However, shadow DOM is required for slotting (composing) components, so typically what I'd recommend for theming is leveraging the array option of each component's styles:

    static styles = [themeStyles, componentStyles]
Then you define your shared styles in `themeStyles`, which is shared across all components you wish to have the same theme.
gitaarik 12/29/2025|||
I also made a simple lib to make it easy to implement common styles in your Lit components:

https://github.com/gitaarik/lit-style

g0wda 12/29/2025|||
oh nice! I didn't know that you can just make it use light dom.

  protected createRenderRoot() {
    return this;
  }
And that's what it takes! I like using tailwind/utility classes so for the styles I'd need to have layers of compiled css files rather than one giant one.
WickyNilliams 12/29/2025||
The major downside of using light DOM is that elements cannot compose neatly since there's no delineating between what the component itself rendered and child content.

When you need to re-render your component, how does the component know not to replace the child content you rendered, Vs the child content it rendered into the light DOM. That's why the shadow DOM and slots were necessary, because then there's no intermingling of your content Vs the component's

This may not be a problem if you don't intend to compose your components. But if you do you will hit limits quickly

spankalee 12/29/2025|||
There's a long standing standards issue for this: https://github.com/WICG/webcomponents/issues/909

While you can easily turn rendering to shadow DOM off on a per-component basis, that removes the ability to use slots. It only really works for leaf nodes.

Pulling a stylesheet into every component is actually not bad though. Adopted stylesheets allow you to share the same stylesheet instance across all shadow roots, so it's quite fast.

ianbutler 12/29/2025||||
Interesting, I'd be curious to know why you all decided not to go with it if you're open to sharing! Minimally to know if I should look at any other promising frameworks.
sleazebreeze 12/29/2025||
I see a good case for my company to use Lit for creating complex components such as highly interactive panels/widgets to be shared between React/Angular apps in our large ecosystem. However the decision was: 1. Prefer sharing plain JS/TS over framework code so try that first and 2. if the component is so complex and tricky to get right, it probably needs to be re-implemented in each framework anyways (or some sort of wrapper)

My secondary concern with Lit is the additional complexity of using shadow and light DOM together in long lived React/Angular apps. Adding a new paradigm for 75+ contributors to consider has a high bar for acceptance.

ianbutler 12/29/2025||
Ah yeah, definitely a much different equation when introducing this across many apps as a shared component library. Mixing different DOM abstractions together could get tricky for sure.

And yes attempting to add a new paradigm for that many people is I am sure quite the task. More political than technical in many ways as well.

Thanks for sharing!

rubenvanwyk 12/29/2025|||
What did you go with?
crazygringo 12/29/2025||
But there's no real reason to, and it just adds confusion around which elements are semantic -- bringing formatting, functionality, meaning to screen readers and search engines, etc. -- vs which are custom and therefore carry no semantic meaning.

If there's no native semantic tag that fits my purposes, I'd much rather stick to a div or span as appropriate, and identify it with one (or more) classes. That's what classes are for, and always have been for.

Support for custom HTML elements seems more appropriate for things like polyfills for actual official elements, or possibly more complicated things like UX widgets that really make sense conceptually as an interactive object, not just CSS formatting.

Using custom element names as a general practice to replace CSS classes for regular formatting just feels like it creates confusion rather than creating clarity.

yawaramin 12/29/2025||
So, the article doesn't discuss this, but there's actually a really good reason to make up and use custom elements: the browser can hydrate their dynamic behaviour automatically. For example, suppose you have:

    <div class=expander>
      <button aria-expanded=false>Expand</button>
      <!-- Some other stuff here -->
    </div>
And you have some JS that handles the expander's behaviour:

    for (const expander of document.querySelectorAll('.expander')) {
      const btn = expander.querySelector('button');

      btn.addEventListener('click', () => {
        btn.ariaExpanded = 'true';
      });
    }
This will work fine for `.expander` divs that are already in the page when the event handler is set up. But suppose you dynamically load new expander divs, what then? Your event handler is not going to retroactively set up their click listeners too.

Custom elements solve exactly this problem. You can now do:

    <expander-elem>
      <button aria-expanded=false>Expand</button>
      <!-- Some other stuff here -->
    </expander-elem>
And then set up the listener:

    customElements.define('expander-elem', class extends HTMLElement {
      connectedCallback() {
        const btn = this.querySelector('button');

        btn.addEventListener('click', () => {
          btn.ariaExpanded = 'true';
        });
      }
    });
And the browser will ensure that it always sets up the listeners for all of the expanders, no matter whether they are loaded on the page initially or dynamically injected later. Without this you would have had to jump through a bunch of hoops to ensure it. This solves the problem elegantly.
dannye 12/29/2025|||
this.querySelector will return nothing when you define this Web Component before (light)DOM is parsed, because the connectedCallback fires on the opening tag.

Above code will only work when the Web Component is defined after DOM has parsed; using "defer" or "import" makes your JS file execute after DOM is parsed, you "fixed" the problem without understanding what happened.

I blogged about this long time ago: https://dev.to/dannyengelman/web-component-developers-do-not...

yawaramin 12/29/2025||
Well, that's why I include JS files at the bottom of my HTML body, to make sure to avoid exactly this problem: https://github.com/yawaramin/dream-html-ui/blob/92f2dfc51b75...
dannye 12/31/2025||
One drawback.

► execute <script> at bottom of file

► execute <script defer>

Both do the same; they execute script after DOM was parsed. When your JS creates GUI you now have to battle FOUCs.

► "import" loads your script async

so it _could_ load before _all_ DOM has parsed... but 9999 out of 10000 scenarios it won't

yawaramin 1/9/2026||
That's why my JS doesn't create the GUI, it just attaches event handlers to the GUI rendered on the server. Check the link I posted above...
rasso 12/29/2025||||
Beware that connectedCallback runs _every time_ a custom element is added to the dom. So you should make sure to only add event listeners once by tracking internally if the element was already initialized.
yawaramin 12/30/2025||
Thanks, that's good to know. In some contexts I don't think a custom element is ever added to the DOM more than once, eg in htmx I am responding with HTML markup which is then injected into the page and then just kept there, possibly 'forever' or at least until it is replaced by some new markup from a new response.
kcrwfrd_ 12/29/2025||||
Yes, the hydration behavior of custom elements is nice. You don’t even need to do anything special with JS bundle loading.

Simply render your <element> (server-side is fine) and whenever the JavaScript downloads and executes your custom elements will mount and do their thing.

crazygringo 12/29/2025|||
That's good to know, and I even did say that custom UX widgets are a case where custom tag names makes more sense conceptually.

I'm really just pushing back on the idea of using them for CSS formatting purposes of general text and layout instead of classes.

veqq 12/29/2025|||
> no real reason

Doing this for syntax highlighting on https://janetdocs.org/ shrank the homepage's .html from from 51kb to 24kb, or 8kb to 6kb compressed (at the time).

gitaarik 12/29/2025||
Why would it create confusion? Because you're not familiar with it? Things change..
crazygringo 12/29/2025||
Because there are so many native elements to keep track of now.

If you see an unfamiliar tag that has a reasonably simple name, you simply don't know if it's native or for formatting.

That's confusing. It's taking two categories of things and mixing them so you can't tell them apart.

gitaarik 12/29/2025||
There's a very clear rule for it: if it contains a dash it's a custom element
crazygringo 12/29/2025||
That rule doesn't work in reverse. If there's no dash it could be either.

So now you've got to try to enforce some practice of using hyphens in all tag names that used to be class names, even if they're a single word that has no place for a hyphen?

It's getting even more confusing now, you see? Not less.

Just use classes. That's what they're there for.

gitaarik 12/30/2025||
Only native elements can be a single word, you can't create a custom elements without a dash.
crazygringo 12/30/2025||
Let's be clear: this whole conversation isn't about custom elements. This is about formatting. Not web components.

You just use it (single word) and style it (CSS) and it works. You don't have to "create" anything.

So nothing's stopping anyone from using single-word elements. There's another comment here defending exactly that:

https://news.ycombinator.com/item?id=46418090

But it's bad practice. Just use classes. It's literally what they are designed for.

gitaarik 12/31/2025||
Aha fair enough, but yeah browsers often allow invalid HTML to just work. But that doesn't mean that the valid usage is somehow flawed. I can understand that the dash can be a bit of a weird rule if you're new to HTML. But I think it's an elegant way to allow users to create custom tags and have readable compact semantic HTML.

But yeah I also do see the confusion part, if a person new to HTML sees these custom tags being used it might think it can create one too and not realize it must contain a dash, and it would still work. So yeah in that sense, indeed it's confusing if you're unfamiliar with it.

But having multiple </div></div></div> at the end of a large block of HTML is also confusing, often resulting in closing too many / too few divs. Having </nav-item></nav-bar></main-header> is much better. So yeah it has it's pros and cons I guess.

donatj 12/29/2025||
I was there like fifteen - twenty years ago before the hyphen standard and shadow dom and all that when JS libraries briefly leaned into just making up their own elements. I think everyone collectively agreed that it was a bad idea and it fell out of popularity.

If I were going to use them, I'd be inclined to vendor them just to prevent any sort of collision proactively

    <donatj-fancy-element>
        <donatj-input />
    </donatj-fancy-element>
zahlman 12/29/2025||
I also saw this at https://lyra.horse/blog/2025/08/you-dont-need-js/#fn:3 :

> You are allowed to just make up elements as long as their names contain a hyphen. Apart from the 8 existing tags listed at the link, no HTML tags contain a hyphen and none ever will. The spec even has <math-α> and <emotion-> as examples of allowed names. You are allowed to make up attributes on an autonomous custom element, but for other elements (built-in or extended) you should only make up data-* attributes. I make heavy use of this on my blog to make writing HTML and CSS nicer and avoid meaningless div-soup. ↩

(HN filtered out a "face with heart eyes" emoji from the second example.)

superkuh 12/29/2025||
https://blog.jim-nielsen.com/2023/html-web-components/ - Jim Nielsen has a series of posts on how to use made up HTML tags in a way that doesn't just fail to nothing when the javascript defining the custom-element doesn't get successfully executed.
dtagames 12/29/2025||
This is the basis of web components and of all popular frameworks. In this model, the only use for a plain <div> is content that doesn't need special reusable layout or behavior. Everything cool gets promoted to a custom component.

What makes this awesome is that no future version of HTML can make your custom component stop working; it's supported down at the "bare metal" level.

I wrote an article [0] a couple years ago about how and why this came to be.

0: https://levelup.gitconnected.com/getting-started-with-web-co...

matt7340 12/29/2025||
This is what Angular does, where an Angular component is typically rendered as a custom tag. I find it to be one of the (very) few nice things about Angular, as it can be helpful to track down components in a large codebase. I haven’t used React for many years, but makes me wonder if custom tags as a convention would be similarly useful.
Devasta 12/29/2025||
You've always been able to do it, doesn't even need to be HTML, serialise as XHTML and you can include the tags on your own namespace and have separate CSS for them.

<!DOCTYPE html>

<html

        xmlns="http://www.w3.org/1999/xhtml"

        xmlns:ns1="mynamespace"

        xmlns:ns2="yournamespace"
>

<body>

<CustomElement>Hello</CustomElement><!-- Custom element in the XHTML namespace -->

<ns1:CustomElement>Hello</ns1:CustomElement>

<ns2:CustomElement>Hello</ns2:CustomElement>

<style type="text/css">

    @namespace ns1 "mynamespace";

    @namespace ns2 "yournamespace";

    CustomElement {

        color:green;

    }

    ns1|CustomElement {

        color:red;

    }

    ns2|CustomElement {

        color:blue;

    }
</style>

</body>

</html>

veqq 12/29/2025||
That's how https://janetdocs.org/ does syntax highlighting:

``` <pre><code class="janet">(<special>defn</special> <symb>bench</symb> <str>`Feed bench a wrapped func and int, receive int for time in ns`</str> [<symb>thunk</symb> <symb>times</symb>] (<special>def</special> <symb>start</symb> (<built-in>os&#x2F;clock</built-in> <keyword>:cputime</keyword> <keyword>:tuple</keyword>)) ```

culi 12/29/2025|
That's kinda cool but the article addresses a major concern with this strategy that is not addressed here. Which is that many of these tags (e.g. <special>, <keyword>, etc) might someday become part of the HTML standard.

The article states that anything with a dash is guaranteed not to be and another commenter here shared their strategy that involved a naming convention like <x-special>, <x-symb>, etc. Perhaps substituting x for j would make sense and alleviate the concern of possible future clashes with web standards

veqq 12/29/2025||
Fair, I had a collision with <math> for a month before someone else noticed some minor error.
kure256 12/29/2025|
The things should be as easy as possible.

The goal is to make content readable by anything. As more users access information through systems like ChatGPT instead of visiting websites directly, content that isn’t easily interpreted by AI crawlers risks becoming effectively invisible.

see: https://github.com/ai-first-guides

gamer191 12/29/2025||
Not sure about you, but I personally prefer my websites not to be able to be plagiarised by AI
allknowingfrog 12/29/2025||
ChatGPT makes the content visible and the author invisible. Why should anyone optimize for AI consumption?
More comments...