--- title: "Reactive HTML without React" author: "@chris" author_url: https://elements.dev/u/@chris published: 2026-11-07T00:45:49.440Z url: https://elements.dev/blog/reactive-html-without-react-ssy8d08n kind: post format: video video: https://elements.dev/blog/reactive-html-without-react-ssy8d08n/video.mp4 poster: https://elements.dev/blog/reactive-html-without-react-ssy8d08n/poster.jpg duration: "24:19" transcript_vtt: https://elements.dev/blog/reactive-html-without-react-ssy8d08n/transcript.vtt --- # Reactive HTML without React by [@chris](https://elements.dev/u/@chris) ยท 2026-11-07 ## Description Plain HTML with a few lightweight extensions, not React and not JSX. Reactive automatically, rendered on the server, updating the DOM directly with no tree diffing, and built into the TypeScript compiler so templates are real symbols with real type checking. ## Notes Elements ships with its own reactive HTML language. It is plain HTML, with TypeScript expressions in braces and templates that take typed attributes the way a function takes parameters. Change the data and the page updates. There is no React, no virtual DOM and no state hooks. This post is the short tour. The video above goes deeper, with the React comparison and a look at how the runtime does it. Here is a complete page. ```ehtml app/pages/home/template.ehtml

{title}

``` The `()` after the tag name declares the *constructor attributes*. They are typed like function parameters. `title` is public, so the route or a parent template sets it. `count` is private, so the template owns it and callers cannot set it. Both are reactive automatically. The `{count}` in the button body updates when the click handler increments it, and nothing else on the page is touched. There is no `useState`, no setter, no effect and no dependency list to get wrong. The route fills the public attributes, often from the database. ```ts app/pages/home/index.ts import { Request, Response } from "@elements/app"; import html from "./template"; export default function route(req: Request, res: Response) { return new html({ title: req.params.title ?? "Hello World!" }); } ``` The page renders on the server, so the first response is the finished HTML. The runtime then attaches to that HTML in the browser. It does not throw the page away and rebuild it. From there the page is reactive: handlers run in the browser, and changes patch the DOM in place. Any top-level tag with a capitalized name is a *template*. Templates take constructor attributes too, and you call one like an HTML tag. ```ehtml app/pages/home/template.ehtml

{greeting}

``` Templates are TypeScript symbols. Pass an attribute the template does not declare, or the wrong type, or leave out a required one, and the build reports an error. Rename and find references work across templates in your editor the same way they work across functions. Loops and conditions are attributes on the element itself, prefixed with `e:`. ```ehtml app/pages/home/template.ehtml

no users yet

{users.length} users

``` `e:for` iterates any iterable and keys the rows by `id`, so when one row changes, one row is patched, and a row that moved is moved rather than re-rendered. `e:if`, `e:elseif` and `e:else` are siblings. There is an `e:switch` with `e:case` and `e:default` for the longer cases. Form inputs are bound both ways. Type in the input and the variable changes. Change the variable and the input changes. ```ehtml app/pages/home/template.ehtml
error = login(form)}>

{error}

``` That is the login form, nothing left out. The same form in React needs a `useState` for each field, an `onChange` handler for each input and a setter call in each handler. Here the reactive parts are the expressions in braces, and the runtime does the rest. A class attribute takes an array, and falsy entries are dropped, so a class comes and goes with a boolean. ```ehtml app/pages/home/template.ehtml ``` Under the hood there is no virtual DOM. The runtime knows which data each binding read, and when that data changes it patches that one text node, attribute or element. There is no component function to re-run, no tree to rebuild and no diff to compute. Work is proportional to the change, not to the size of the page. The page's attributes are live in the browser console as `$`. Assign to one and the page updates through the same path the template uses, which is a quick way to see any state of the UI without clicking through it. ```js browser console > $.title = "Boom!"; ``` The language is built into the TypeScript toolchain inside the Elements project server, so a template is a TypeScript symbol. Go to definition, find references and rename work across templates in your editor the same way they work across functions, and every attribute is type checked. Why not React? When this language started in 2012, React did not exist yet. The idea was that a user interface should reflect its data automatically, and that the reactivity belongs in the runtime, not in the code you write. React moved the other way. Getting started means learning a stack of abstractions, hooks, effects, dependency lists and re-render rules, and the APIs keep changing. Beginners get lost in it, and experienced developers still get it wrong in ways that show up as bugs and slow pages. I wanted our users to write HTML and have the runtime do the rest. That decision turned out to matter for AI agents. An agent has seen far more HTML than JSX, and with lexically scoped constructor attributes and automatic reactivity it spends its effort on the presentation rather than on rendering mechanics. In practice that means fewer tokens and fewer bugs in the interface. Humans find the code easier to read too. Every new app also installs Elements UI, a style system with about 80 templates for the parts every application needs: forms, overlays, data tables, charts, chat. The agent builds well with the raw language alone, and the templates mean it does not reinvent a date picker every time. The manual has the full language at [elements.dev/man/html](https://elements.dev/man/html), and the templates are at [elements.dev/ui](https://elements.dev/ui). Questions go in the comments. ## Transcript Hey everybody, in this video I'm going to show you around the reactive HTML language that ships with Elements. You should recognize it because it's just HTML with some lightweight extensions that make it capable of handling reactive data. And this has been in the making for many years. It started in 2012, it was the original purpose of the Elements toolchain was to build this reactive language. And after that it evolved into the entire toolchain for building applications. But it started as the HTML language, and so this has a special place in my heart. I can't wait to walk you through it. Let me give you a quick demo of how it works first, and then I can also walk you through the history of it and how we've done integration with TypeScript, how it compares to React, and why I think you'll find it interesting, both for human development and also for your agents, which I think will have a much easier time with this language. So over on the left in my editor, you can see that it should look pretty familiar. This is just an HTML file essentially with an import statement that it's a little bit weird because it's importing a style sheet CSS style sheet, but it's typescript. We're just writing a regular typescript import statement. So you can write typescript Right alongside the HTML inside this file and that is fine Now down here. This is an HTML tag We call this a template and this is a special template That is the HTML template and you can write it just like this and it will be exported from this file as the default export Templates are you can put any kind of HTML in here and right here. I've got a body tag and an h1 tag and so far this is just Basic HTML if I want to create a new template I can create one by opening a tag and I'll give it a capital case to name. Let's just call this greeting and And inside of that, I can put any HTML I want. So why don't I go ahead and put this h1 tag inside of there. And then I can call that greeting child like this. And when I save the browser, update it automatically. Now you didn't see anything change because we didn't change the greeting. But it did update. So you can create as many templates as you want. You can create templates here in this file. You can create them in a different file and import them like this. And so it makes it very easy to organize and refactor your templates. If you're just working on a page, you can just put all the templates in one file and not worry about it. As the application gets bigger and bigger, you want to share templates across your pages. You can now refactor those into a shared folder and that will work as well. Okay, now let's do something a little bit more interesting. Let's add some reactive data. Now, we've added something to the HTML language, some lightweight extensions. The idea here is we want HTML to be primary. You should be looking at this file and thinking this is essentially an HTML file. It's not JSX, it's not Vue, it's not a custom language that sort of sprinkles in HTML. It's just HTML with a couple of syntax extensions. The first one that you need to know about are called constructor attributes. So at the opening tag for a template, you can use constructor -like syntax. Notice the parentheses. So it's kind of like you're building a constructor function or something like that. And inside of that, we can declare constructor attribute names. We can make them public. We can make them private. And by default, they will be public. That means that when you invoke the template or you call out to the template, you can provide a value for that. So, let's create one that's public by default called greeting and it will be of type string and its default value will be something like a holo universe that we see a change. And then I want to thread that through to the greeting template. So I can do this or for short you can just use the identity expression. And now I need to add that as a parameter to greeting. Let me actually just save this and show you something quickly. down at the bottom I've got out the element start program running and so you can see build errors down here and you can see that we we actually have an error it's telling me the template greeting has no attribute named greeting so the first thing to note about this is that these templates are type checked and the reason they're type checked is because we built the HTML language into directly into the TypeScript tool chain so when you're writing HTML code it looks like the HTML language but it has the symbols and all the type checking that would expect from writing regular TypeScript. So what this is telling us is that there's no attribute called greeting, so we need to define that, and we can create a constructor attribute here, and it'll just be the same. It's greeting. It's going to be public by default. In this case, I'm not going to give it an initializer value, so the caller will have to provide it. And then inside of the h1 tag, I'm going to render that out and save it. And you'll see that the browser over on the right updates almost instantly from the hot module reload. So we've learned a couple of new things here. We've got this constructor attribute syntax that's been added. You can have private or public attributes. Notice if I make this private, I'll now get a compiler error because a type checking error that says that the template greeting cannot accept the argument for private attribute greeting. So the attribute exists but it's private. I cannot pass it in. I can only modify it or use it within the template itself. So we'll go ahead and go ahead and make it public again, and now we're back to the green status. Okay a green state Okay, so we've learned about constructor attributes and also about these the expression block syntax whenever you see a curly brace like this, it means that it's an expression block and you can put any typescript expression in there the Constructor attributes are kind of like function parameters. They're lexically scoped, which means if you can see it inside of the structure it is available for you to use so I can see it up here that means I can use it here and these constructor attributes are reactive automatically you don't need to think about that so if you're passing in an attribute and that attribute changes the user interface will automatically update as fast as it possibly can with whatever the new value is. One of the cool things you can do to play around with this is go over into the browser And there's a special global, the dollar sign global, that is bound to the attributes of the top level template. And I can change the value of that constructor attribute directly in my console, which is super useful for debugging, or if you're building different states in your user interface, you can see those different states just by changing this value. So let's change it to something iconic like boom. And there we go. The user interface has updated automatically with that new value. So I find that that ability to change the data, see that the user interface updates right away is a much more effective way to work with the user interface than working with abstractions like runes or some function call you have to make that's not accessible and we can't really hack that very well. Okay, so we've got a very basic HTML template here with a child template called greeting. We've threaded through this constructor attribute. Now, next up, let's get rid of the default value there, press Save, and we're going to be back to an error now. What this is showing us is that the route function, if you're used to working with other web frameworks, you know about routes, and in Elements, the route is a function. It's actually in code. We don't use any kind of special magic with your file system or directory paths or anything like that. It's just actually a function, so let's go find it. Home index and this is what the function looks like. It has a request and a response It kind of looks like a regular node .js server request response function And we're going to return a new instance of this HTML You can think of it as a class really and where did that come from? We imported it as the default from the template file. So HTML the HTML symbol is Exported here as the default in any name templates will be exported as a name So if we wanted to get access to that named one, we could do something like this. But for routes, we generally just, we import the HTML. And this now needs to take an attribute. So what it's telling us is that the error is that it's expecting one argument, it got none. So let me give it an empty object. Now we have a new error that says, actually, it also needs greeting. All right, we've got to come up with something new now. So there we go. So we can see it change. Okay so we're passing data from the route into the HTML file and when I save it the browser over on the right is updated right away. Now going up one more level where does this route actually get used? This isn't supposed to be a video about routing but I just want to give you a sense of what's happening so it's not so magical. Notice that the route function gets called on the application and the URL this is the home URL is mapping to this home function which we imported up here and the home function is that route that we we're just looking at. All right let's go back to HTML. Now let's say that you want to bind to a form. Oftentimes when we're working with forms the data it's kind of complicated to keep the user interface in check or in synchronize in sync with the data but in Elements that happens automatically. So if I create a function let me actually just wipe this out and start fresh here. So let me walk through this. We have an interface that's just a normal TypeScript interface called form and it has a field called email and our form has a default initializer in the constructor attributes so we don't have to pass it in through the route. For now we just want to demonstrate the two -way binding. And then down in the input underneath the form tag we have a value that is set to form .email. So what we do here is the input is going to be bound automatically, it's value to form email, so if I were to put something like hello at Elements dev, that will show up in the form field. But it's automatically two -way bound, and that means that if I change this, let's just change it to say example, and let me go down into the console and type form email, that has now been automatically updated as well. So the data stays in sync with the user interface. And if I change the data here, I don't know, what do we want to do? Support, how about that? Elements .dev. Note the user interface up in the top updates. That's what it means to be two -way bound. And I just wanted to give you a quick sense of that here. We don't have to go through every single example. You can read the manual for that. But hopefully, this gives you a sense of how that works. All right, next up, let's look at event handlers and how we change state by responding to some kind of event. So let's create a toggle button. And the toggle button will be something like this. We're gonna add an event handler on click. And when we click this, I want to set this constructor attribute, which I'll create in a second, to the opposite of whatever it was before and what we'll do is if the constructor attribute active is true or truthy then we'll add the is primary class CSS class to the button now this is a little bit of special new syntax here so let me to walk you through it whenever you see the braces it means you're about to see some typescript expression inside of there if you put an array you could have multiple items here for example we might have something like toggle and then the second one and the third one and fourth one. If any of the items are false, so for example, let's say active is false, the right -hand side of this expression will not evaluate and this will be false and we'll drop that from the class list. So this is a nice convenient way of being able to string together a bunch of different CSS classes automatically. And if any of them are false, we just drop it rather than putting like false or undefined or something like that into the class list. Okay, so I'll leave that. Let's just delete it so we can see the full line and we're gonna get an error because we haven't defined active yet So let's create that as a private variable this time private constructor attribute And I'll call it active. It's of type boolean and by default by default. We'll just set it to false Great now Notice what we've done here in the on click event handler we've done an assignment and we're changing the value of this variable to This syntax here says, if it was true before, make it false. If it was false before, make it true. Make it the opposite of what it was before. As soon as this value changes, that's going to reactively update the class that we apply. And that will change the styling of this toggle button. All right, let me save and make sure that this works. So come over and we'll click the button. And now it gets that special styling for isPrimary. Let's open up Elements, different Elements. This is the HTML elements down here and see that we can see it so see the class is primary Now, let me click it again, and now it goes away Okay, so that's the data re the user interface reacting Automatically to the reactive data and this active is Accessible inside this callback function because it's in scope So we say that that's lexically scoped and for the uninitialized what that means is if you can see it it's just like a function. Think of it like a function that has parameters. And anywhere in the function, however many nested blocks you have inside the function, the parameters that are above are visible all the way down that the chain. So this active variable is visible here. Now, if you wanted to, you could also pass that to a function. So we might say on click and we're going to pass it a active and you might return the new value. And I could assign that to the result like this. And notice I'm passing the value and then I'm assigning the new value to the constructor attribute here. If I pass the value and change it inside of this function, it's not going to update properly, just for the same reason that you can't change any primitive values inside of a JavaScript function. And so you can either assign it as the return value or I can pass an object like the form that has active as a field and then you can update it that way. So let's make sure this still works. There we go. Okay, so this is just a quick tour of the HTML language and its syntax And I think it's a really, really great way to write sophisticated user interfaces that react automatically to data. This is something that's been proven, a concept that's been proven over the past decade to be a very effective way to write complicated applications. And all the applications I've written, and some of which are very, very large, and financial service applications, and healthcare services, and things like that, and you end up with tons and tons and tons of pages. And you need a way to organize them in a way so that you can just keep building without worrying about the structure getting completely out of control so you really need a way of organizing user interface functionality into essentially functions or templates and this does a really good job of that now why not react so when we started in 2012 react actually didn't exist yet so Elements began as an HTML language believe it or not but the first sketches I wish I could show you a sketch but I've got it in my notebook from 2012 and the reason I did that was because I thought the way to build user interfaces with reflecting a state machine or reflecting data that changes automatically, the user interface was a really, really good idea. But you also wanted to have things like server side rendering. If you look at these pages, if I do a view page source, might be a little hard to see, but you'll see that the HTML is rendered directly from the server first. So there's no blank page that then gets hydrated on the client. You always get the server side HTML first, and then it gets attached in the browser automatically. That's how HTML works, and you don't have to configure that. It just works automatically. There's no hydration errors that are possible by construction. The other thing we wanted to do is to make sure that the reactivity is pushed into the runtime, such that whether it's your agent working or you're working, when you're writing HTML, you don't want to be thinking about these abstractions, reactivity abstractions. That's a lot of cognitive overhead to think through that, and you can get it wrong, and you can have performance problems. So we really just wanted you to declare, using HTML, what it should look like and have the data very clearly in the attributes or in these expression blocks, and then the runtime and the compiler takes care of making sure that everything is updated automatically as performantly as possible. But TypeScript has JSX and React baked in, so it would be natural for us to support React and to just have it be the default. But what we found was that it's very hard for first of all beginners to get started with React because there's an insane number of abstractions. And the APIs change a lot. There's issues with the runtime being not performant in some cases. But really the hard part is just getting started and working your way around React and JSX is I found not to be a good user experience. And I had numerous issues with it. And when I was teaching back in the day, people would get very confused by what was happening. So I just felt like it wasn't a great user experience and I didn't want our users to have to struggle with that. I made that decision before AI even became a big deal. And once AI became a big deal, we thought, well, now it really makes sense to have a new HTML language because the React kind of lock -in effect from ecosystem size and all that just doesn't really matter anymore. So the agent obviously knows HTML. It has more HTML on its training data than it has JSX. And to be clear, I can write JSX fine. React obviously has a lot of struggle with HTML, this is not a problem. So the new HTML language and Elements works really, really well with agents. But what was surprising was they actually do better with it because the lexically scoped constructor attributes, the automatic reactivity, all of that lets the agent focus exclusively on the presentation and just writing out that presentation code and not on higher order abstractions. And so that saves a little bit on tokens, and also empirically we found gives you less buggy user interfaces. And it's also, as a side benefit, just easier, we believe, for humans to read this code. So all in all, a syntax that's essentially just HTML with some lightweight extensions, all of the reactivity and complexity is pushed into the runtime, so the runtime makes sure that it does the most performant thing every time by construction, or at least that's That's the goal. I think we achieve it in most cases, and we don't want the user to have to think about re -rendering loops or anything like that. There's no diffing trees that happen in Elements. It just updates surgically the specific piece of UI that has to be updated. And we wanted it to be very debuggable and easy to work with, hence being able to go into the console here and just update the data and see your user interface update. So the last part of that became, well, what about all the extra user interface templates that exist and components that exist in the React ecosystem. And don't we want to be able to use that? And there's two answers to that. So first, you'd be surprised at how effective an agent is at writing extremely complicated HTML. It can do that very, very well. And so even with no templating library, even before we built Elements UI, which I'll tell you about in a second, we found that the agents worked very, very well with HTML as a with Elements HTML as a primitive and so it could build very complicated things automatically but just to be sure and also because we don't think reinventing everything every single time makes a lot of sense there are a number of user interface elements that essentially every application needs and so we took many months to build a professional style system and also roughly between 70 80 something of the order of that templates that are reusable that you can use. So you should recognize if you're in the React community or any of the other communities, you'll tend to see these recurring types of templates, like button, button group. If you scroll down the left -hand side here, you'll see various types of overlays, displays. And we even have some complicated ones, like we have a data table that you can use to replace spreadsheets. So if you're trying to build an application, if you've built something in Retool, for example, or one of the low code or no code user interface builders, you'll recognize something like this data table. We've got a Kanban. I don't know if I pronounced that correctly. We've got charts and area charts, bar charts. We've got stuff for chat, for prompting. So if you're building something that is kind of like an AI prompt, you can use that. So we've got all kinds of reusable templates and a style system that lets you build up a beautiful user interface and it gets out of your way. So if you want to just use the style system directly, you can use that. You can build your own templates that way. So we try to build this so that the abstractions do not get in the way of a human or an agent, but we have enough of the things built such that you can make ferocious progress right away. So all in all, I believe this is probably the best way today to build user interfaces on the web. And it's a super exciting time for me to release this because I've spent so many years in this language. You would think that given the HTML language itself already exists, that there wouldn't be too much work to do, but it turns out there's a lot of things you have to think through. Special thanks to the Microsoft team for porting their TypeScript to the Golang language. We've made extensive use of that and we built this into the TypeScript tool chain such that the symbols are just TypeScript symbols. If you, in my editor here, try to go to the reference of any of these templates, it will take you right there. You can do a find replace. This is just thought of as TypeScript code under the hood. And that's what gives us these strongly typed constructor attributes and with a little bit of extra syntax for things like the constructor attribute definitions. But really, this is just TypeScript with an HTML extension to the TypeScript language built directly into the tool chain. So I hope this gives you a really, probably more comprehensive than I expected going into this overview of Elements HTML. And let me know if you've got any questions in the comments.