Lustre v5.2.0 released!

This release brings Lustre up to date with the wider Gleam ecosystem, making Lustre compatible with the v1 releases of both gleam_erlang and gleam_otp. We also have some improvements to event handling, testing, and the companion lustre_dev_tools package now uses Tailwind v4!

Better conditional event handling

With the release of v5 we moved event handlers to a declarative decoder-based approach. Instead of handling the event object directly, you provide a decoder for the message to produce and can use modifiers like event.prevent_default to prevent an event’s default behaviour. This worked well but we found that it made some behaviour possible in v4 now no longer possible in v5. Specifically, you could not conditionally choose to call event.prevent_default based on the event’s data.

This release adds a new event.advanced handler that can be used to indicate whether or not event.prevent_default or event.stop_propagation when the event is handled, making it possible to implement handlers such as preventing the default behaviour when the key property of the event is "Enter" but not other keys.

pub fn chat_input(
  value value: String,
  on_input handle_input: fn(String) -> msg,
  on_submit handle_submit: msg,
) -> Element(msg) {
  let on_keydown = event.advanced({
    use key <- decode.field("key", decode.string)
    use shift <- decode.field("shiftKey", decode.bool)
    use value <- decode.subfield(["target", "value"], decode.string)

    case key {
      "Enter" if shift ->
        decode.success(event.handler(
          dispatch: handle_input(value <> "\n"),
          prevent_default: True,
          stop_propagation: False,
        ))

      "Enter" ->
        decode.success(event.handler(
          dispatch: handle_submit,
          prevent_default: True,
          stop_propagation: False,
        ))

      _ ->
        decode.success(event.handler(
          dispatch: handle_input(value),
          prevent_default: False,
          stop_propagation: False,
        ))
    }
  })

  html.textarea([
    attribute.value(value),
    attribute.on_keydown(on_keydown),
  ])
}

In the above snippet we implement a chat input that supports submitting a message when the user presses the Enter key, and inserting a new line in the input when the user presses Shift + Enter.

Better assertion-based testing

Gleam v1.11 introduced the new assert keyword that will panic if the condition is not met and is now the de facto way to write tests in Gleam. To help write better tests, Lustre v5.2 adds the query.matches and query.has functions for making assertions about an element.

pub fn greeting_test() {
  let element = greeting("Lucy")
  let selector = query.text("Hello, Lucy!")

  assert element |> query.matches(selector:)
}

pub fn dashboard_test() {
  let element = dashboard(User("Lucy"))
  let selector = query.tag("h2") |> query.and(query.text("Welcome, Lucy!"))

  assert element |> query.has(selector:)
}

Tailwind v4 support

Lustre Dev Tools is a companion package you can use for zero-config development and building of Lustre applications. Part of that zero-config magic is automatic support and detection for projects using Tailwind CSS.

Tailwind v4 was released in January but up until now Dev Tools users have had to make do with v3. This release updates the binary that Lustre Dev Tools uses, which means a move to a CSS-based configuration and simpler support for Tailwind class detection.

Existing users can continue to use the JavaScript tailwind.config.js config by following the instructions in the Tailwind upgrade guide. For most users adopting the new version will be painless, but there are a small number of breaking changes you should make sure you check out before upgrading!

And the rest

Lustre’s dependency constraints for gleam_erlang and gleam_otp have been brought up to date with the recent v1 releases of those packages, meaning apps using server components can update and keep up with the latest core libraries. We’ve also vendored happy-dom to help us test Lustre’s client runtime: starting with tests of the reconciler. And, of course, another round of bug fixes and docs improvements have been made!


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