What should a native DOM templating API look like?

62 jdkoeck 31 7/1/2025, 5:40:57 PM justinfagnani.com ↗

Comments (31)

polyrand · 3h ago
After trying a bunch of ways of creating client-side apps, I've now also settled on using lit-html (not even the full Lit framework). Just the `html` and `render` functions. It's simple, convenient, and fast.

Since it "caches" the rendered parts, I haven't had performance issues just re-rendering everything on state changes (with some basic scheduling). I find it easier to reason about than React since I can use Vanilla JS everywhere, and components are just functions using the `html` tagged template, which can be composed.

vlucas · 2h ago
Good writeup. I am in the camp that Tagged Template Literals are the ultimate answer for easy templating that is native to JavaScript. The bit about using native Signals with them is spot on too. Nothing else really allows the clean separation of HTML strings with other random JavaScript objects and pieces as easily as Tagged Templates do. On the remarks about fear of strings, server-rendered markup is just one giant string sent to the browser anyways. I don't know why some people get so worked up about why they think templates should not be strings. That is literally what HTML shipped to the browser is. It's also the easiest and most straightforward way to define templates in native JavaScript without any compile step.

I did many of my own experiments with templates that are native JS with no compile step, and reached basically the same conclusion as this post. Use tagged templates and keep it simple. My angle was server-focused, with async bits in a template doing automatic streaming while server rendering with them. Works super well and it takes so little code to achieve. Check it out if you're interested: https://www.hyperspan.dev/ or the CodeSandbox demo: https://7lpdpl.csb.app/

jfagnani · 2h ago
Interesting server framework! It looks very similar in some ways to a server I started last year out of frustration with Koa and Express called Zipadee: https://github.com/justinfagnani/zipadee?tab=readme-ov-file#...

The templates look basically identical. html-tagged template literals that support streaming and async values and automatically escape values.

What Koa does will allowing strings by be returned with a default HTML mimetype is security malpractice, IMO. It's way to easy to just interpolate user-controlled values.

maxloh · 4h ago
I don't understand why Svelte isn't even mentioned. Its syntax is more readable for me.

  <div>
    {#each colors as color, i}
      <button
        style="background: {color}"
        aria-label={color}
        aria-current={selected === color}
        onclick={() => selected = color}
      >
        {i + 1}
      </button>
    {/each}
  </div>
jfagnani · 3h ago
Author here. I could include Svelte, but honestly it's still like the others: markup with embedded binding expressions and control flow. It would only bolster my claim that popular template syntaxes are similar.
cluckindan · 3h ago
I think you meant to use color in a variable interpolation instead of ”red”. Now the code only makes red buttons.
maxloh · 2h ago
Yeah, you're right.

The code came directly from Svelte's official tutorials. Looks like I accidentally copied the problem instead of the solution!

roywiggins · 3h ago
I'm somewhat partial to Alpine, which sort of crams it into html.

    <div x-data="{ open: false }">
        <button @click="open = true">Expand</button>
 
        <span x-show="open">
            Content...
        </span>
    </div>
cluckindan · 3h ago
x-anything is just painful like Angular. UI state is better off being non-declarative, otherwise the builtin behaviors become a limit and unmaintainable workarounds ensue.

Nobody wants to express complex conditions all around the UI when derived state can be handled in a single store.

rictic · 5h ago
Discussion on the previous post in this series: https://news.ycombinator.com/item?id=44390452
bevr1337 · 3h ago
> Nested function calls (like React's createElement() or Hyperscript's h()) or object-based builder-style APIs just don't look enough like HTML.

What is "enough" here? It is good enough for the unified community. I know npmtrends.com isn't the best metric, but unified is popular. Unified has made it trivial to move between HTML, MDX, MD, JSX and even combinations of the above. I'm learning it's an opinion, but hyperscript and hast are HTML enough being that they're models of HTML

Hooking into a tagged template is a challenge. Functions will receive a list of unparsed html strings and already evaluated expressions to zipper up themselves before forwarding to the downstream template API.

https://stackoverflow.com/questions/39971088/evaluating-temp...

Unlike an expression or tag, a developer can hack on any AST or hyper script provided by a library. Im not sure proxies or reflection are useful working with string primitive and they're unergonomic.

This was something I mentioned on the other post so I appreciate seeing it called out clearly in the reply. (Not to assume I inspired the mention.) Thank you for sharing.

jfagnani · 3h ago
Author here: Those don't look like HTML enough because they're not markup. Every single one of the top frameworks uses markup with interpolations as their basic template format: React, Vue, Angular, Preact, Lit, Svelte, Solid, Quik, Stencil, Marko, Polymer, FAST, and on and on.

Frameworks and rendering libraries that don't have a markup-based template syntax are just very rare.

And I think for understandable reasons: Markup-based templates look like the output, and web developers know HTML, so the templates are easier to read and write.

That StackOverflow question seems unrelated because it's asking about untagged template literals. With tagged template literals, depending on the return type, you can absolutely get to the underlying values.

bevr1337 · 2h ago
> That StackOverflow question seems unrelated because it's asking about untagged template literals.

The first answer reinvents tagged templates and the second suggests them. I thought it was a good example to show the complexity of hacking in this approach. I'll add more context in the future.

I'm agreed that the average developer would hate working with an an AST -- JSX covers 99.9% use-case and React codebases are full of functional programming escape hatches -- but it is the foundation. An HAST is an interoperable, extendable representation we can move between any framework or tooling.

I'm likely projecting too much on the proposal. Templating is the top priority but it's the ancillary bits you mention that excite me most.

> Markup-based templates look like the output

I made the mistake of drinking functional programming laced koolaid, so in my mind they really do translate well, but I respect that you have a broader perspective.

Thanks for taking the time to reply. I'm a smidge honored that you'd even review my feedback!

MrJohz · 1h ago
My biggest criticism of this design is that the author doesn't seem to have explored how Svelte, Vue Vapor or SolidJS work at all. The design seems largely based on the idea that rendering the template is a cheap operation, and that it can be done repeatedly whenever values change. So it's fine to have something like html`<div>${arr.map(el => html`...${el}...`)}</div>`, because each time the array changes, rendering every item in the list is a cheap operation. And it's typically cheap because it's rendering to a VDOM-like structure, and the more expensive diffing happens later.

But modern signal-based frameworks tend to work on the basis that doing these of rendering for each change is unnecessary, and so you should only update the relevant part of the DOM. This means rendering as seldom as often, in particular when handling conditionals and lists. This is why both SolidJS and Vue Vapor both have helpers for rendering lists and conditional elements, because if you just use `.map` and the ternary operator, things aren't going to work as you'd expect. Svelte has its own syntax but in principle works very similarly.

This proposal feels like it's very oriented around the React/Lit VDOM mechanism, with a handful of Preact-like concessions to signals where they are relatively easy to retrofit into a VDOM-like system. The problem is that these sorts of frameworks are heavyweight, slow, cumbersome things, because they're doing so much extra work. Codifying that sort of inefficiency directly into the browser seems like a terrible idea.

My second biggest criticism of this proposal is that I'm really struggling to see the benefit of it, i.e. who it's for. It can't be for framework compatibility, because it's not addressing any of the issues that actually make frameworks incompatible with each other (different state/context mechanisms, different lifecycles, etc). It can't be to codify common rendering idioms and optimise them by letting the browser implement them natively, because, as discussed, the proposal doesn't leave a lot of room for well-optimised frameworks. So I guess the idea is to ensure that the browser has a built-in web framework to make it easier for people to build complex web applications without needing to import lots of dependencies.

But if you're building a complex web application, the sort that needs specialised rendering like this, the overhead of a web framework is probably pretty minimal anyway. And if you're building simpler sites with minor interactivity, this sort of rendering system is complete overkill. You'd be better if writing a wrapper around document.createElement and going from there.

rglover · 3h ago
I think I've cracked this [1]. A lot of the popular frameworks just copy each other by using funky attributes, templating hacks, and compilers. You don't need that (yes, they have certain positives and negatives but you don't need them).

Instead, what's hinted at in this article (using a plain HTML string) works great. Add in a little abstraction for the sake of structure and simplicity and you've got a surprisingly robust UI framework without a ton of complexity.

[1] https://cheatcode.co/joystick (a full-stack JS framework that has its own components API)

cluckindan · 3h ago
At a glance, this seems like a clone of React with react-router v6. How is it different?
rglover · 2h ago
Not really like React at all (and certainly not React Router). Not sure how you made that connection...

The component method in the framework is inspired by React v1 (really just that it's a function you call—my approach to everything else is different), but all of the routing is on the server-side (a light abstraction that maps back to Express.js routes).

cluckindan · 2h ago
Maybe it was the lifecycle hooks. :)
rglover · 2h ago
If you're going to evaluate a piece of technology at least take the time to play with it. This isn't just unhelpful to me, it's doing a disservice to yourself (assuming that I've created something that will help you and improve how you do your work).
cluckindan · 1h ago
Sorry, I’m on vacation and only have my phone with me. I will check it out later for sure!
svieira · 4h ago
The piece that I don't like about this is trying to standardize how to "spell" setting attributes, properties, and events in a single namespace by using sigils. JavaScript isn't Perl or K and the whole `@` vs. `.` vs `?` feels alien to the language (in my opinion). It also doesn't feel like something that could be used correctly by a component author who wanted to pass props _through_ the component they own to another component they don't.

I would expect to use something more like namespaces, where the default would be shorthand `on:`, `attr:`, `prop:`, `propOrAttr:` (maybe this is the default?) but where an explicit namespace declaration could move them if you for whatever reason needed to support XHTML 5.1 and your document already used the `on:` namespace for something else (not that _that_ is a thing right now, but why not be prepared?)

jfagnani · 3h ago
Author here. Property and event disambiguation syntax is definitely far on the easy to change side of things.

I personally think the sigils are easier to read and write than perfixes, and these particular sigils have popped up independently in multiple libraries like Vue, Lit, and LighterHTML.

But this API could use prefixes, or like Vue, it could support both.

hk1337 · 4h ago
Kudos to the large text. I was thinking of doing similar on my blog.
dvh · 3h ago
It should be secure by default, no more .innerHTML = user_name and gluing strings together like with SQL in the '90s
jfagnani · 2h ago
This API is definitely secure by default, and that's one of the constraints and requirements I mention in the post.

The API is secure because it separates static developer controlled strings from dynamic and possibly user-controlled values by JavaScript syntax. Values from text bindings are written to the DOM by setting TextNode.data, which escapes the value first.

RadiozRadioz · 4h ago
I think it would look a lot like XSLT
righthand · 3h ago
I got into XSLT after the HN grug brain article last week. It is wonderful and I have my whole site (100 pages/files) migrated in less than an hour (w/o using LLM) and have it served off a 4 line caddy file.

Seriously browsers just need to adopt XSLT 3.0 and stop all this wheel reinvention already.

monknomo · 2h ago
That sounds interesting, and I'd like to hear more
righthand · 2h ago
Well it’s all in a single directory:

```

static/

  js/
  
  css/

  log/

    my-post.xml

    my-post.xsl

  index.html
```

Caddyfile:

```

:8080

try_files {path}.xml {path}.html

file_server {

    index index.xml index.html

    root static
}

```

XSLT work is all based off of a repository someone wrote with a grug brain explainer:

https://github.com/pacocoursey/xslt

The next step is to generate all of the `static` dir from markdown files and probably a new markdown-like format for registering templates to generate xsl/xml files for special html blocks and template parts or generating blog listing pages.

LegionMammal978 · 2h ago
Eh, I also thought that at one point (regarding XSLT 3.0), but it turns out that parts of it are an underspecified mess. E.g., you can specify static parameters to be supplied to the document, but it has nothing on when or how any type-conversion errors would be signaled [0]. And in general, it has very imperative features like shadow attributes that purists would turn up their noses at. I don't see it becoming widespread anytime soon.

[0] https://github.com/Paligo/xee/issues/32#issuecomment-2762343...

timewizard · 3h ago
My favorite templating library has always been amrita[0]. I really don't like template languages that try to mix code and elements into a single block of text. It mixes together two concerns and creates exceptionally fragile blobs that generally need to be fully rewritten to extend their functionality. Whereas with amrita you can generally keep the two entirely separate which makes a huge difference in practice.

These PHP style template ideas I have never understood. I thought we all agreed 30 years ago that while exceptionally functional for a "quick and dirty" approach they are horrible for long term product or application development.

You have a DOM. Use it as such.

[0]: https://github.com/rud/amrita