I switched from Htmx to Datastar

2025-10-106:49315380everydaysuperpowers.dev

In 2022, David Guillot delivered an inspiring DjangoCon Europe talk, showcasing a web app that looked and felt as dynamic as a React app. Yet he and his team had done something bold. They converted it…

In 2022, David Guillot delivered an inspiring DjangoCon Europe talk, showcasing a web app that looked and felt as dynamic as a React app. Yet he and his team had done something bold. They converted it from React to HTMX, cutting their codebase by almost 70% while significantly improving its capabilities.

Since then, teams everywhere have discovered the same thing: turning a single-page app into a multi-page hypermedia app often slashes lines of code by 60% or more while improving both developer and user experience.

I saw similar results when I switched my projects from HTMX to Datastar. It was exciting to reduce my code while building real-time, multi-user applications without needing WebSockets or complex frontend state management.

The pain point that moved the needle

While preparing my FlaskCon 2025 talk, I hit a wall. I was juggling HTMX and AlpineJS to keep pieces of my UI in sync, but they fell out of step. I lost hours debugging why my component wasn’t updating. Neither library communicates with the other. Since they are different libraries created by different developers, you are the one responsible for helping them work together.

Managing the dance to initialize components at various times and orchestrating events was causing me to write more code than I wanted to and spend more time than I could spare to complete tasks.

Knowing that Datastar had the capability of both libraries with a smaller download, I thought I’d give it a try. It handled it without breaking a sweat, and the resulting code was much easier to understand.

I appreciate that there’s less code to download and maintain. Having a library handle all of this in under 11 KB is great for improving page load performance, especially for users on mobile devices. The less you need to download, the better off you are.

But that’s just the starting point.

Better API

As I incorporated Datastar into my project at work, I began to appreciate Datastar’s API. It feels significantly lighter than HTMX. I find that I need to add fewer attributes to achieve the desired results.

For example, most interactions with HTMX require you to create an attribute to define the URL to hit, what element to target with the response, and then you might need to add more to customize how HTMX behaves, like this:

<span hx-target="#rebuild-bundle-status-button"
      hx-select="#rebuild-bundle-status-button"
      hx-swap="outerHTML"
      hx-trigger="click"
      hx-get="/rebuild/status-button"></span>

One doesn’t always need all of these, but I find it common to have two or three attributes every timeAnd then there are the times I need to remember to look up the ancestry chain to see if any attribute changes the way I’m expecting things to work. Those are confusing bugs when they happen! .

With Datastar, I regularly use just one attribute, like this:

<span data-on-click="@get('/rebuild/status-button')"></span>

This gives me less to think about when I return months later and need to recall how this works.

How to update page elements

The primary difference between HTMX and Datastar is that HTMX is a front-end library that advances the HTML specification. DataStar is a server-side-driven library that aims to create high-performance, web-native, live-updating web applications.

In HTMX, you describe its behavior by adding attributes to the element that triggers the request, even if it updates something far away on the page. That’s powerful, but it means your logic is scattered across multiple layers. Datastar flips that: the server decides what should change, keeping all your update logic in one place.

To cite an example from HTMX’s documentation:

<div>
   <div id="alert"></div>
    <button hx-get="/info" 
            hx-select="#info-details" 
            hx-swap="outerHTML"
            hx-select-oob="#alert">
        Get Info!
    </button>
</div>

When the button is pressed, it sends a GET request to /info , replaces the button with the element in the response that has the ID 'info-details', and then retrieves the element in the response with the ID 'alert', replacing the element with the same ID on the page.

This is a lot for that button element to know. To author this code, you need to know what information you’re going to return from the server, which is done outside of editing the HTML. This is when HTMX loses the ”locality of behavior” I like so much.

Datastar, on the other hand, expects the server to define the behavior, and it works better.

To replicate the behavior above, you have options. The first option keeps the HTML similar to above:

<div>
    <div id="alert"></div>
    <button id="info-details"
     data-on-click="@get('/info')">
        Get Info!
    </button>
</div>

In this case, the server can return an HTML string with two root elements that have the same IDs as the elements they’re updating:

<p id="info-details">These are the details you are looking for…</p>
<div id="alert">Alert! This is a test.</div>

I love this option because it’s simple and performant.

Think at the component level

A better option would change the HTML to treat it as a component.

What is this component? It appears to be a way for the user to get more information about a specific item.

What happens when the user clicks the button? It seems like either the information appears or there is no information to appear, and instead we render an error. Either way, the component becomes static.

Maybe we could split the component into each state, first, the placeholder:

<!-- info-component-placeholder.html -->
<div id="info-component">
    <button data-on-click="@get('/product/{{product.id}}/info')">
        Get Info!
    </button>
</div>

Then the server could render the information the user requests…

<!-- info-component-get.html -->
<div id="info-component">
    {% if alert %}<div id="alert">{{ alert }}</div>{% endif %}
    <p>{{product.additional_information}}</p>
</div>

…and Datastar will update the page to reflect the changes.

This particular example is a little wonky, but I hope you get the idea. Thinking at a component level is better as it prevents you from entering an invalid state or losing track of the user’s state.

…or more than one component

One of the amazing things from David Guillot’s talk is how his app updated the count of favored items even though that element was very far away from the component that changed the count.

David’s team accomplished that by having HTMX trigger a JavaScript event, which in turn triggered the remote component to issue a GET request to update itself with the most up-to-date count.

With Datastar, you can update multiple components at once, even in a synchronous function.

If we have a component that allows someone to add an item to a shopping cart:

<form id="purchase-item"
      data-on-submit="@post('/add-item', {contentType: 'form'})">"
>
  <input type=hidden name="cart-id" value="{{cart.id}}">
  <input type=hidden name="item-id" value="{{item.id}}">
  <fieldset>
    <button data-on-click="$quantity -= 1">-</button>
    <label>Quantity
      <input name=quantity type=number data-bind-quantity value=1>
    </label>
    <button data-on-click="$quantity += 1">+</button>
  </fieldset>
  <button type=submit>Add to cart</button>
  {% if msg %}
    <p class=message>{{msg}}</p>
  {% endif %}
</form>

And another one that shows the current count of items in the cart:

<div id="cart-count">
    <svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg">
        <use href="#shoppingCart">
    </svg>
    {{count}}
</div>

Then a developer can update them both in the same request. This is one way it could look in Django:

from datastar_py.consts import ElementPatchMode
from datastar_py.django import (
    DatastarResponse,
    ServerSentEventGenerator as SSE,
)

def add_item(request):
    # skipping all the important state updates
	return DatastarResponse([
		SSE.patch_elements(
    		render_to_string('purchase-item.html', context=dict(cart=cart, item=item, msg='Item added!'))
		),
		SSE.patch_elements(
    		render_to_string('cart-count.html', context=dict(count=item_count))
		),
	])

Web native

Being a part of the Datastar Discord, I appreciate that Datastar isn’t just a helper script. It’s a philosophy about building apps with the web’s own primitives, letting the browser and the server do what they’re already great at.

Where HTMX is trying to push the HTML spec forward, Datastar is more interested in promoting the adoption of web-native features, such as CSS view transitions, Server-Sent Events, and web components, where appropriate.

This has been a massive eye-opener for me, as I’ve long wanted to leverage each of these technologies, and now I’m seeing the benefits.

One of the biggest wins I achieved with Datastar was by refactoring a complicated AlpineJS component and extracting a simple web component that I reused in multiple placesI’ll talk more about this in an upcoming post. .

I especially appreciate this because there are times when it’s best to rely on JavaScript to accomplish a task. But it doesn’t mean you have to reach for a tool like React to achieve it. Creating custom HTML elements is a great pattern to accomplish tasks with high locality of behavior and the ability to reuse them across your app.

However, Datastar provides you with even more capabilities.

Apps built with collaboration as a first-class feature stand out from the rest, and Datastar is up to the challenge.

To accomplish this, most HTMX developers achieve updates either by “pulling” information from the server by polling every few seconds or by writing custom WebSocket code, which increases complexity.

Datastar uses a simple web technology called Server-Sent Events (SSE) to allow the server to “push” updates to connected clients. When something changes, such as a user adding a comment or a status change, the server can immediately update browsers with minimal additional code.

You can now build live dashboards, admin panels, and collaborative tools without crafting custom JavaScript. Everything flows from the server, through HTML.

Additionally, suppose a client’s connection is interrupted. In that case, the browser will automatically attempt to reconnect without requiring additional code, and it can even notify the server, “This is the last event I received.” It’s wonderful.

Just because you can do it doesn’t mean you should

Being a part of the Datastar community on Discord has helped me appreciate the Datastar vision of making web apps. They aim to have push-based UI updates, reduce complexity, and leverage tools like web components to handle more complex situations locally. It’s common for the community to help newcomers by helping them realize they’re overcomplicating things.

Here are some of the tips I’ve picked up:

- Don’t be afraid to re-render the whole component and send it down the pipe. It’s easier, it probably won’t affect performance too much, you get better compression ratios, and it’s incredibly fast for the browser to parse HTML strings.

- The server is the state of truth and is more powerful than the browser. Let it handle the majority of the state. You probably don’t need the reactive signals as much as you think you do.

- Web components are great for encapsulating logic into a custom element with high locality of behavior. A great example of this is the star field animation in the header of the Datastar website. The <ds-starfield> element encapsulates all the code to animate the star field and exposes three attributes to change its internal state. Datastar drives the attributes whenever the range input changes or the mouse moves over the element.

But you can still reach for the stars

But what I’m most excited about are the possibilities that Datastar enables. The community is routinely creating projects that push well beyond the limits experienced by developers using other tools.

The examples page includes a database monitoring demo that leverages Hypermedia to significantly improve the speed and memory footprint of a demo presented at a JavaScript conference.

The one million checkbox experiment was too much for the server it started on. Anders Murphy used Datastar to create one billion checkboxes on an inexpensive server.

But the one that most inspired me was a web app that displayed data from every radar station in the United States. When a blip changed on a radar, the corresponding dot in the UI would change within 100 milliseconds. This means that *over 800,000 points are being updated per second*. Additionally, the user could scrub back in time for up to an hour (with under a 700 millisecond delay). Can you imagine this as a Hypermedia app? This is what Datastar enables.

How it’s working for me today

I’m still in what I consider my discovery phase of Datastar. Replacing the standard HTMX functionality of ajaxing updates to a UI was quick and easy to implement. Now I’m learning and experimenting with different patterns to use Datastar to achieve more and more.

For decades, I’ve been interested in ways I could provide better user experiences with real-time updates, and I love that Datastar enables me to do push-based updates, even in synchronous code.

HTMX filled me with so much joy when I started using it. But I haven’t felt like I lost anything since switching to Datastar. In fact, I feel like I’ve gained so much more.

If you’ve ever felt the joy of using HTMX, I bet you’ll feel the same leap again with Datastar. It’s like discovering what the web was meant to do all along.


Read the original article

Comments

  • By David-Guillot 2025-10-109:185 reply

    Thanks to Chris to continue challenging his comfort zone (and mine!) and sharing his impressions and learnings with us!

    I may be a little biased because I've been writing webapps with htmx for 4 years now, but here are my first thoughts:

    - The examples given in this blogpost show what seems to be the main architectural difference between htmx and Datastar: htmx is HTML-driven, Datastar is server-driven. So yes, the API on client-side is simpler, but that's because the other side has to be more complex: on the first example, if the HTML element doesn't hold the information about where to inject the HTML fragment returned by the server, the server has to know it, so you have to write it somewhere on that side. I guess it's a matter of personal preference then, but from an architecture point-of-view both approaches stand still

    - The argument of "less attributes" seems unfair when the htmx examples use optional attributes with their default value (yes you can remove the hx-trigger="click" on the first example, that's 20% less attributes, and the argument is now 20% less strong)

    - Minor but still: the blogpost would gain credibility and its arguments would be stronger if HTML was used more properly: who wants to click on <span> elements? <button> exists just for that, please use it, it's accessible ;-)

    - In the end I feel that the main Datastar selling point is its integration of client-side features, as if Alpine or Stimulus features were natively included in htmx. And that's a great point!

    • By nymanjon 2025-10-1015:211 reply

      The article stated that he no longer needs eventing to update other parts of the page, he can send down everything at once. So, I guess that is much less complex. Granted, eventing and pulling something down later could be a better approach depending on the circumstance.

      • By yawaramin 2025-10-113:111 reply

        You can send everything down at once with htmx too, with oob swaps.

        • By throwaway7783 2025-10-114:142 reply

          yes you can, but the complexity is now moved to server side template wrangling. With SSE, its just separate events with targets. It feels much cleaner

          • By yawaramin 2025-10-115:44

            Server side template wrangling is not really a big deal, if you use an HTML generation library...something like Python's Hpty/FastHTML or JavaScript's JSX. You can easily split the markup down into 'components' and combine them together trivially with composition.

          • By andersmurphy 2025-10-117:141 reply

            I mean in practice you rarely target individual elements in datastar. You can sure. But targeting the main body with the entirety of the new content is way simpler. Morph sorts out the rest

            • By throwaway7783 2025-10-1115:511 reply

              A good example is when a page has expensive metrics specific to say a filter on the page. Let's say an action on the page shows a notification count change in the top right corner.

              While morph will figure it outz it's unnecessary work done on the server to evaluate the entire body

              • By andersmurphy 2025-10-1118:511 reply

                Expensive queries on the server should be shared where they can be (eg: global leaderboard) or cached on the server (in the game of life demo each frame is rendered/calculated once, regardless of the number of users). Rendering the whole view gives you batching for free and you don't have to have all that overhead tracking what should be updated or changed. Fine grained updates are often a trap when it comes to building systems that can handle a lot of concurrent users. It's way simpler to update all connected users every Xms whenever something changes.

                • By throwaway7783 2025-10-121:091 reply

                  I agree on caching. But in general my point stands. The updates in question may not even be shared across users, but specific to one user.

                  Philosophically, I agree with you though.

                  • By andersmurphy 2025-10-128:35

                    Yeah so that was how I used to think about these things. Now, I'm. less into the fine grain user updates too.

                    Partly, because the minute you have a shared widget across users 50%+ of your connected users are going to get an update when anything changes. So the overhead of tracking who should update when you are under high load is just that, overhead.

                    Being able to make those updates corse grain and homogeneous makes them easy to throttle so changes are effectively batched and you can easily set a max rate at which you push changes.

                    Same with diffing, the minute you need to update most of the page the work of diffing is pure overhead.

                    So in my mind a simpler corse grain system will actually perform better under heavy load in that worst case scenario somewhat counter intuitively. At least that's my current reasoning.

    • By arethuza 2025-10-1013:372 reply

      "Alpine or Stimulus features were natively included in htmx"

      I'm contemplating using HTMX in a personal project - do you know if there are any resources out there explaining why you might also need other libraries like Alpine or Stimulus?

      • By AstroBen 2025-10-1013:441 reply

        They're for client-side only features. Think toggling CSS classes, updating the index on a slider- you ideally don't want to have to hit the server for that

        • By arethuza 2025-10-1013:51

          Thanks - I was having a quick read of the documentation for those projects and that makes perfect sense.

      • By scragz 2025-10-1019:02

        if you use alpine, make sure to get the morph extensions for both htmx and alpine.

    • By melvinroest 2025-10-1012:111 reply

      Reminds me a bit of the Seaside framework in Pharo. A lot of the things I programmed in Pharo at my previous employer was a lot of back and forth between front-end and back-end, because the back-end was managing the front-end state. For B2B apps that don't have a lot of latency requirements, etc., I'd say it's better. For high scalable B2C apps though? No.

      • By swores 2025-10-1012:302 reply

        Could you expand on why you think it (back-end managing the front-end's state) is better in the scenarios that you do?

        Edit - rather than spam with multiple thank you comments, I'll say here to current and potential future repliers: thanks!

        • By skydhash 2025-10-1012:45

          Not GP, but I would say, it’s the same reason someone would use React. If you keep you state in a single place, the rest of the app can become very functional and pure. You receive data and tranform it (or render it). The actual business logic that manipulate the state can be contained in a single place.

          This reduces a lot of accidental complexities. If done well, you only need to care about the programming language and some core libraries. Everything else becomes orthogonal of each other so cost of changes is greatly reduced.

        • By rapind 2025-10-1012:43

          I would imagine the same arguments for Smalltalk like live coding and an IDE within your production application. So you get some overlap with things like Phoenix LiveView, but more smalltalk-y.

          I assume it had backend scaling issues, but usually backend scaling is over-stated and over-engineered, meanwhile news sites load 10+ MB of javascript.

    • By taffer 2025-10-1122:05

      > htmx is HTML-driven, Datastar is server-driven

      As far as I understand, the main difference between HTMX and datastar is that HTMX uses innerHTML-swap by default and datastar uses the morph-swap by default, which is available as an extension for HTMX [1].

      Another difference is that datastar comes with SSE, which indeed makes it server driven, but you don't have to use SSE. Also datastar comes with client-side scripting by default. So you could say the datastar = integrated HTMX + idiomorph + SSE + Alpine.

      [1] https://htmx.org/extensions/idiomorph/

    • By stronglikedan 2025-10-1014:082 reply

      > if the HTML element doesn't hold the information about where to inject the HTML fragment returned by the server, the server has to know it, so you have to write it somewhere on that side

      I'm not too strong in frontend, but wouldn't this make for a lighter, faster front end? Especially added up over very many elements?

      • By sudodevnull 2025-10-1015:20

        100%. Datastar is just make HTML spec support reactive expression in data-* attributes, that's it. You will become stronger at web cause it just gets out of your way

      • By yawaramin 2025-10-113:16

        I don't think the difference would be significant. How many of your HTML elements would become interactive with htmx? There's a limit to how much interaction you can reasonably add on a page. This will also limit the number of new attributes you will introduce in the markup.

        Also, by this argument should we leave out the 'href' attribute from the '<a>' tag and let the server decide what page to serve? Of course not, the 'href' attribute is a critical part of the functionality of HTML.

        Htmx makes the same argument for the other attributes.

  • By andersmurphy 2025-10-1011:044 reply

    Fantastic write up!

    For those of you who don't think Datastar is good enough for realtime/collaborative/multiplayer and/or think you need any of the PRO features.

    These three demos each run on a 5$ VPS and don't use any of the PRO features. They have all survived the front page of HN. Datastar is a fantastic piece of engineering.

    - https://checkboxes.andersmurphy.com/

    - https://cells.andersmurphy.com/

    - https://example.andersmurphy.com/ (game of life multiplayer)

    On both the checkboxes/cells examples there's adaptive view rendering so you can zoom out a fair bit. There's also back pressure on the virtual scroll.

    • By wild_egg 2025-10-1013:581 reply

      If I understand the code for these correctly though, you're not actually doing the "idiomatic" datastar things as the article describes? No diffing/patching individual elements, just rerender the entire page?

      Tbh that mental model seems so much simpler than any or all of the other datastar examples I see with convoluted client state tracking from the server.

      Would you build complex apps this way as well? I'd assume this simple approach only works because the UI being rendered is also relatively simple. Is there any content I can read around doing this "immediate mode" approach when the user is navigating across very different pages with possibly complicated widget states needing to be tracked to rerender correctly?

      • By andersmurphy 2025-10-1016:04

        I mean Datastar is pretty flexible. I'd say CQRS is pretty idiomatic if you want to do multiplayer/realtime stuff. As you mentioned, once you've se that up, the mental model is much simpler. That being said the initial set up is more involved than req/response Datastar.

        Yes we are building complex accounting software at work with Datastar and use the same model. "Real UI" is often more complex, but a lot less heavy less divs, less data, fewer concurrent users, etc compared to these demos. Checkboxes are a lot more div dense than a list of rows for example.

    • By zestyping 2025-10-1117:301 reply

      Can you explain how these work? Does the server send small subrectangles of the large grid when the user scrolls to new regions of the grid? Does the browser actually have a two-dimensional array in memory with a billion items, or is there some other data structure?

      • By andersmurphy 2025-10-1118:28

        Yeah the server only sends what the user is currently looking + plus a buffer around their view. There's no actual checkbox state on the client. When the user clicks a checkbox a depress animation is started and a request is made (which the server responds to with no data and a 204). The user then gets the html for the next view down a long lived SSE connection that started when they first loaded the page. Because, there's a long lived connection, it has really good compression. Same thing happens when the user scrolls. If they scroll far enough a new view is rendered.

        The billion items themselves are just in a server on the backend, stored in a sqlite database.

    • By dandersch 2025-10-1014:151 reply

      > On both the checkboxes/cells examples there's adaptive view rendering so you can zoom out a fair bit.

      how do you zoom out?

      Also, even with your examples, wouldn't data-replace-url be a nice-to-have to auto update the url with current coordinates, e.g. ?x=123&y=456

      • By andersmurphy 2025-10-1015:58

        Currently zoom the page web cmd+/-. At some point I'll add buttons and to proper quantised views.

    • By liotier 2025-10-1012:123 reply

      > think you need any of the PRO features

      Pro features ? Now I see - it is open core, with a $299 license. I'll pass.

      • By andersmurphy 2025-10-1012:15

        Good for you!

        I don't use anything from pro and I use datastar at work. I do believe in making open source maintainable though so bought the license.

        The pro stuff is mostly a collection of foot guns you shouldn't use and are a support burden for the core team. In some niche corporate context they are useful.

        You can also implement your own plugins with the same functionality if you want it's just going to cost you time in instead of money.

        I find devs complaining about paying for things never gets old. A one off life time license? How scandalous! Sustainable open source? Disgusting. Oh a proprietary AI model that is built on others work without their consent and steals my data? Only 100$ a month? Take my money!

      • By rvitorper 2025-10-1017:38

        It is 299$ lifetime. It is extremely cheap

  • By leg100 2025-10-109:312 reply

    I don't think the article does a good job of summarising the differences, so I'll have a go:

    * Datastar sends all responses using SSE (Server Side Events). Usually SSE is employed to allow the server to push events to the client, and Datastar does this, but it also uses SSE encoding of events in response to client initiated actions like clicking a button (clicking the button sends a GET request and the server responds with zero or more SSE events over a time period of the server's choice).

    * Whereas HTMX supports SSE as one of several extensions, and only for server-initiated events. It also supports Websockets for two-way interaction.

    * Datastar has a concept of signals, which manages front-end state. HTMX doesn't do this and you'll need AlpineJS or something similar as well.

    * HTMX supports something called OOB (out-of-band), where you can pick out fragments of the HTML response to be patched into various parts of the DOM, using the ID attribute. In Datastar this is the default behaviour.

    * Datastar has a paid-for Pro edition, which is necesssary if you want certain behaviours. HTMX is completely free.

    I think the other differences are pretty minor:

    * Datastar has smaller library footprint but both are tiny to begin with (11kb vs 14kb), which is splitting hairs.

    * Datastar needs fewer attributes to achieve the same behaviours. I'm not sure about this, you might need to customise the behaviour which requires more and more attributes, but again, it's not a big deal.

    • By lioeters 2025-10-1020:08

      As someone on the sideline who's been considering HTMX, its alternatives and complements, this was a helpful comment! Even without having used any of it, I get the feeling they're going in the right direction, including HTMX author's humorous evangelism. If I remember correctly he also wrote Grug, which was satire and social criticism of high caliber.

    • By nchmy 2025-10-1016:571 reply

      some quibbles

      D* doesnt only use SSE. It can do normal http request-response as well. Though, SSE can also do 0, 1 or infinite responses too.

      Calling datastar's pro features "necessary" is a bit disingenuous - they literally tell people not to buy it because those features, themselves, are not actually necessary. Theyre just bells and whistles, and some are actually a bad idea (in their own words).

      Datastar is 11kb and that includes all of the htmx plugins you mentioned (sse, idiomorph) and much more (all of alpine js, essentially).

      • By leg100 2025-10-1017:371 reply

        > Calling datastar's pro features "necessary" is a bit disingenuous

        I didn't. I said:

        > * Datastar has a paid-for Pro edition, which is necesssary if you want certain behaviours. HTMX is completely free.

        I don't need to spell out why this means something very different to what you think it means.

        I'll happily concede on the other two quibbles.

        • By nchmy 2025-10-1018:11

          Sorry, i can see how i misinterpreted the "necessary" part.

HackerNews