Lustre v5.3.0 released!

With this release we introduce two exciting new capabilities to Lustre: the ability to teleport elements to other parts of the DOM with lustre_portal, and a new context api that allows components to pass data to slotted children. We’ve also made some significant improvements to Lustre’s virtual DOM, making it more resilient to external changes to the DOM from browser extensions.

Teleporting elements with lustre_portal

A common feature in many frontend frameworks is the ability to teleport or portal elements to a different part of the DOM, letting your application maintain a logical tree structure even when the realised DOM structure is different.

To bring this feature to Lustre, we’ve published a new package called lustre_portal which provides a component that can be used by both client Lustre applications and server-rendered HTML to teleport children to a different part of the DOM.

A common use-case for portals is to render toasts and alerts into the <body> of a page, guaranteeing that they are always on top of any other content. Let’s see what that looks like with lustre_portal:

fn view(model: Model) {
  html.div(
    [attribute.class("w-screen h-screen flex items-center justify-center")],
    [
      // ... your main application content ...

      // These toasts are rendered into the `<body>` element outside of our app.
      // That way they always appear on top of any other content.
      portal.to("body", [], [
        keyed.fragment({
          use toast, i <- list.index_map(list.take(model.toasts, max_toasts))
          let key = int.to_string(toast.id)
          let toast = view_toast(toast.message, i)

          #(key, toast)
        }),
      ]),
    ],
  )
}

If you inspected the HTML of this page, you would see toasts rendered directly into the body:

<body>
    <div id="app">
        <!-- ... your main application content ... -->
    </div>

    <p>This is a toast</p>
    <p>This is another toast</p>
</body>

In the package repo you can find complete examples on how to implement tooltips, have multiple interactive islands on a page, and how to render Lustre elements into other JavaScript libraries.

You can add lustre_portal to your new or existing Lustre projects by running:

gleam add lustre_portal

Communicating across component boundaries with context

Compared to other frameworks, components in Lustre are much less common. Instead most state lives in your application’s Model and is passed down to pure view functions. When elements want to communicate back to the application, they emit events.

A powerful aspect of Lustre’s component system is that this method of communication carries through to components: components are real Custom Elements that receive data through attributes and properties and emit events when they want to communicate upwards.

When a component wants to receive children, it can render one or more <slot> elements in its view. When elements are placed inside the component, the browser teleports them into the slot, allowing the component to render complex flexible ui elements without needing to handle its children directly.

This poses a problem: what if the component wants to communicate with its slotted children? Until now the answer to that question has been an unsatisfying mix of emitting events from the component and having the parent application update the rendered children itself.

This approach can work well, and we recommend it in many cases, but there are some drawbacks. What happens if there isn’t a parent application to catch these events in the first place? What if the component and its slotted children are two closely-related components; we might be leaking implementation details by forcing communication through the parent.

To address these issues, Lustre now implements the Web Components Community Group context proposal. Now, when configuring a component you can use the new on_context_change option to signify that this component can receive context data from any parent that provides it:

lustre.component(init:, update:, view:, options: [
  component.on_context_change("wibble-context", {
    use wibble <- decode.field("wibble", decode.string)
    use wobble <- decode.field("wobble", deocde.int)

    decode.success(ParentProvidedContext(wibble:, wobble:))
  })
])

When this component is first rendered, it will dispatch a "context-request" event. Any parent component or application can use the new provide effect to listen for these events and provide context data to the component:

fn init(_) {
  let model = Model(..)
  let effect = effect.provide("wibble-context", json.object([
    #("wibble", json.string("Hello, Joe!")),
    #("wobble", json.int)
  ]))

  #(model, effect)
}

Crucially, these events will bubble up through a component’s <slot> elements, allowing it to provide context to children without knowing about them!

We think this powerful new feature will open up a lot of possibilities for Lustre components, in particular we’re excited to explore two use-cases:

A more resilient VDOM

Lustre applications render a lightweight data structure known as a virtual DOM. A part of the runtime, known as the reconciler, takes that virtual DOM and uses it to efficiently update the browser’s real DOM. Many frameworks adopt this approach but all of them share a common problem: if the real DOM drifts out of sync with the virtual DOM, then applying patches becomes unpredictable and can lead to errors.

This crops up most often when browser extensions like Grammarly or Google Translate modify the real DOM with additional content. To combat this, Yoshi has been hard at work rewriting a large part of the reconciler to take an approach sometimes known as a “concrete virtual DOM”. With a concrete virtual DOM, you no longer use DOM methods to traverse the DOM. Instead, you construct your own tree containing pointers to real DOM nodes and traverse that; only interacting with the DOM when you need to update a node directly. By skipping the DOM traversal step Lustre can keep working when the real DOM structure is modified, and it can even continue to work if an element is removed from the DOM entirely!

This resiliency means that while users may have a degraded experience when using certain extensions, the application itself will continue to work instead of crashing or throwing errors. In the future, we may even detect such cases and try to cooperate with those kinds of plugins ensuring that the content Lustre renders matches what the user sees on their screen.

This technique was in part inspired by the amazing work of Simon Lydell, who has done an incredible job of documenting his work on making the Elm virtual DOM safer.

And the rest

Components saw some additional improvements in this release too. The parts attribute can be used to toggle individual CSS parts on or off in a similar fashion to the classes attribute. Components can also configure the delegates_focus option to allow them to receive focus when they are clicked or interacted with, making it easier to build accessible ui widgets. A handful of missing SVG elements and attributes have been added.

Finally, the move to a concrete virtual DOM has helped us simplify other parts of Lustre’s runtime and caught a number of bugs related to fragments and event handling in the process. This simplification opens the door for future improvements, and we’ll be continuing this work in the future 👀.


Lustre is still largely maintained by me – Hayleigh – with the support of a small number of contributors. To my existing sponsors on GitHub, thank you! Your support has fueled me with both motivation and caffeine to keep the project growing 💕.

If you’re interested in supporting Lustre, one of the best things you can do is build something with it and tell everyone about it!

If you or your company are using Lustre in production, please consider supporting the project financially over on GitHub sponsors.

Search Document