Using Rust and Leptos to build beautiful, declarative UIs
Tags: rust, ui
Published: 2023-12-06 16:12:05

**Written by [Eze Sunday](https://blog.logrocket.com/author/ezesunday/)✏️**

As Rust continues to grow rapidly, so does its ecosystem of tools. One relatively new tool in this ecosystem is Leptos, a modern, full-stack web framework for building declarative and fast UIs with [Rust and WebAssembly (Wasm)](https://blog.logrocket.com/getting-started-with-webassembly-and-rust/). 

Leptos uses a fine-grained reactivity system to efficiently update the DOM directly without the overhead of a [virtual DOM like React's](https://blog.logrocket.com/virtual-dom-react/). Additionally, its isomorphic design simplifies development by allowing you to build both server-side rendered (SSR) and client-side rendered (CSR) applications with a single codebase. 

If you’re coming from the JavaScript world, Leptos is similar to SolidJS, but with the added benefits of Rust's type safety, performance, and security, along with the portability and performance of Wasm. 

In this guide, we will explore how to build UIs with Leptos. We’ll create a demo to-do app to explore this web framework for Rust in detail — you can [find the full source code on GitHub](https://github.com/ezesundayeze/leptos-example). Let’s start by setting up our development environment.

## Setting up Leptos

Since Leptos is a Rust framework, we need to have Rust installed first. [Install Rust with Rustup](https://blog.logrocket.com/getting-up-to-speed-with-rust/#installingrust) using the command below for Unix systems:

```shell
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

If you’re on a Windows machine, you’ll need to install the Windows Rustup installer for your system architecture. 

To verify that Rust is installed correctly, run the following command in your terminal:

```shell
rustc --version
```

The expected output should look like this:

```shell
rustc 1.75.0-nightly (75b064d26 2023-11-01)
```

Now that we have Rust installed, let's set up Leptos. There are two ways to do this, depending on whether you want to build a full-stack SSR app or a CSR app. 

In this guide, we’ll focus on building a client-side rendered application with Leptos and Trunk, is a zero-config Wasm web application bundler for Rust. Install Trunk system-wide by running the following command:

```rust
cargo install trunk
```

Next, initialize a Rust binary application with the command below:

```rust
cargo init todo-app
```

Move into the new Rust application directory you just created and install Leptos as a dependency, with the CSR feature enabled:

```rust
cargo add leptos --features=csr
```

Once that is complete, your app directory structure should look like this:

```plaintext
├── Cargo.lock
├── Cargo.toml
├── src
│ └── main.rs
```

Next, create an `index.html` file in the root directory and add the following basic HTML structure to it because Trunk needs a single HTML file to facilitate all asset building and bundling:

```html
<!DOCTYPE html>
<html>
  <meta charset="UTF-8">
  <head></head>
  <body></body>
</html>
```

Before we continue, make sure you have the `wasm32-unknown-unknown` Rust compilation target installed. This target allows you to compile Wasm code that will run on different platforms, such as Chrome, Firefox, and Safari. 

If you don't have this target installed, you can install it with the following command:

```rust
rustup target add wasm32-unknown-unknown
```

Our setup is now complete. The project structure should look like this:

```plaintext
.
├── Cargo.lock
├── Cargo.toml
├── index.html
├── src
│   └── main.rs
```

Next, let’s construct our first component.

## Understanding the structure of a Leptos component

Components are the building blocks of Leptos applications. They’re reusable, composable, reactive, and named in PascalCase. Leptos components are like UI components in other frontend frameworks like React and SolidJS — there are building blocks for creating complex user interfaces. 

Leptos components take props as arguments, which we can use to configure and render the component. They must also return a `View`, which represents the UI element that the component will render. 

Here is an example of a Leptos component:

```rust
#[component]
fn Button(text: Text) -> impl IntoView {
    view!{
        <button>{text}</button>
    }
}
```

In most cases, you’ll need to add the `#[component]` attribute to the component function. However, this is not necessary if you’re returning the view directly in a closure in the main function, as shown in the following example:

```rust
use leptos::*;

fn main() {
    mount_to_body(|| view! { <p>"Hello, todo!"</p> })
}
```

Otherwise, we’ll have to mount the component like so:

```rust
fn main() {
    mount_to_body(Button);
}
```

Now that we understand how components work in Leptos, let’s get started with our demo Rust app.

## Building a to-do application with Leptos and Rust

Now that we have a good idea of how the component should be, let’s go ahead and build the components for our to-do application. We’ll need three components:

*   `App` — The entry component
*   `InputTodo` — To accept user inputs and add them to the `TodoList`
*   `TodoList` — Will render all the to-dos in the to-do list

The application will allow you to add new to-do items, view them in a list, and delete the to-dos like so: ![User Shown Interacting With To Do App Built With Leptos And Rust. App Has Magenta Background With Title Todo App At Top And Text Input Field Where User Is Shown Typing To Do Items. Items Are Added To List And User Is Shown Hovering Over Items To Reveal X Symbol. Clicking X Symbol Deletes To Do Item From List](https://blog.logrocket.com/wp-content/uploads/2023/11/Leptos-Rust-to-do-app.gif)

### The `App` component

The `App` component will be the root component that we’ll use to compose other components declaratively. It will contain the `TodoInput` and the `TodoList` components, and we’ll pass the `todos` props to it. 

Now, copy the code below and add it to the `main.rs` file. This will be the starting point for the entire application:

```rust
#[component]
fn App() -> impl IntoView {
    let todos = create_signal(vec![]);
    view! {
        <div class="todo-app">
            <h1>"Todo App"</h1>
            <TodoInput initial_todos={todos} />
            <TodoList todos={todos} />
        </div>
    }
}

fn main() {
    leptos::mount_to_body(App);
}
```

The code above might throw a lot of errors because we haven’t defined the `TodoInput` and `TodoList` components yet. 

If you look carefully, you’ll notice the `create_signal` function at the beginning of the `App` function. Leptos uses signals to create and manage the app state, so be prepared to see this function a lot. 

Signals are functions that we can call to get or set their associated component value. When a signal's value changes, all of its subscribers are notified and their associated components are updated. Essentially, signals are the heart of the reactivity system in Leptos:

```rust
let todos: (ReadSignal<Vec<TodoItem>>, WriteSignal<Vec<TodoItem>>) = create_signal(vec![]);
```

The `create_signal()` function returns a getter `ReadSignal<T>` and setter `WriteSignal<T>` pair for a signal. The setter allows you to update the component's state and the getter allows you to read the state. 

We also passed along the `todos` signal to the `TodoInput` and `TodoList` components as props. This means that the `todos` signal will be required as a prop when creating those components. 

Still in that same code above, we've passed a vector of `TodoItem` structs. That means that the state will be a list of `TodoItem` structs. Of course, the state can be of any type, but we’re using a struct format because it allows us to store multiple items effectively. 

So, let's define the `TodoItem` struct with an `id` and the content of the `todo` as shown below:

```rust
#[derive(Debug, PartialEq, Clone)]
struct TodoItem {
    id: u32,
    content: String,
}
```

### The `TodoInput` component

Next, let’s go ahead to create the `TodoInput` component. Copy the code below into your `main.rs` file:

```rust
#[component]
fn TodoInput(
    initial_todos: (ReadSignal<Vec<TodoItem>>, WriteSignal<Vec<TodoItem>>),
) -> impl IntoView {
    let (_, set_new_todo) = initial_todos;
    let (default_value, set_default_value) = create_signal("");
    }
}
```

In the code above, the `TodoInput` component takes a `todos` props as an argument. This will allow us to update the to-do list when a user inputs some text and hits the **Enter** key. Next, we destructure the `initial_todos` and get the `set_new_todo` method, which allows us to update the state. 

In addition to props, we can also create in-component signals to manage the state directly within the component. 

For instance, we created a signal named `default_input_value` to control the default value of the input field. This signal enables us to clear the input field after adding a new to-do, ensuring that the input field is ready for the next item. 

Next, let's create the `input` field and add some properties to it. Add the following code after the second `let` statement in the `TodoInput` component:

```rust
    view! {
        <input
        type="text"
        class= "new-todo"
        autofocus=true
        placeholder="Add todo"
        on:keydown= move |event| {
            if event.key() == "Enter" && !event_target_value(&event).is_empty() {
                let input_value = event_target_value(&event);
                let new_todo_item = TodoItem { id: new_todo_id(), content: input_value.clone() };
                set_new_todo.update(|todo| todo.push(new_todo_item));
                set_default_value.set("");
            }}
            prop:value=default_value
        />
```

You can add virtually any valid HTML attribute to the `input` field, including events. Leptos uses a colon `:` as a separator for events, unlike vanilla HTML, which doesn’t have a separator. For instance, [the `onclick` event in HTML](https://blog.logrocket.com/dynamically-create-javascript-elements-event-handlers/#inline-event-handlers) becomes `on:click` in Leptos code. 

In our case, we need to track when the `on:keydown` down event is fired and handle it. To get the value of a text input field, Leptos provides a special method `event_target_value(&event);` that allows you to get the value of the typed input:

```rust
let input_value = event_target_value(&event);
```

Then, we want to update the state when the user hits the **Enter** button. Leptos provides two primary methods for updating the state and triggering reactivity:

*   Use the `.update()` method when you need to perform additional checks, manipulations, or calculations before updating the state. This method accepts a callback function that receives the current state value and allows you to modify it before the update is applied
*   Use the `.set()` method when you want to directly assign a new value to the state without any additional logic. This method is more concise and efficient for simple state updates

In our example code, we used the `.update()` method — `set_new_todo.update` — because we're dealing with an array of structs. 

Adding an item to an array also requires the `push()` method, which involves more than just assigning a new value. The `.update()` method allows us to perform the `push()` operation within the callback function to ensure the array is updated correctly. 

Finally, you can update the value of the field with the `prop:value` property. This is equivalent to the `value` property in vanilla HTML. 

Now, we have a `TodoInput` component fully set up. When we run the application, the output should look like so: ![Plain White To Do App Shown With Black Text Title Todo App Above Blue Outlined Text Input Component Containing Grey Text Directing User To Add Todo](https://blog.logrocket.com/wp-content/uploads/2023/11/TodoInput-component-e1701279446998.png)

### The `TodoList` component

Next, we'll construct the `TodoList` component, which is responsible for rendering the entire `TodoList` element. Leptos offers two distinct approaches for declaratively rendering lists of items: static rendering and dynamic rendering. 

Static list rendering involves rendering a list of items from a `Vec<T>`. This method is suitable for scenarios where the list items are fixed and known beforehand. Here’s an example:

```rust
let todos = vec!["Eat Dinner", "Eat Breakfast", "Prepare lunch"];
view! {
  <p>{values.clone()}</p>
  <ul>
      {values.into_iter()
          .map(|n| view! { <li>{n}</li>})
          .collect::<Vec<_>>()}
  </ul>
}
```

Considering the code above, all it takes to render the static list is to iterate over it and return it in a view using the `view!` macro. In comparison, dynamic list rendering is specifically designed to be reactive and utilize Leptos' signal system. For this purpose, Leptos provides a dedicated `<For/>` component. 

Given this information, you’ve probably guessed that we’ll be using dynamic list rendering for our `TodoList` component. When we add a new to-do item, we need the list to be updated and re-rendered. 

The code below represents how our `TodoList` component will look. Copy and append it into your `main.rs` file:

```rust
#[component]
fn TodoList(todos: (ReadSignal<Vec<TodoItem>>, WriteSignal<Vec<TodoItem>>)) -> impl IntoView {
    let (todo_list_state, set_todo_list_state) = todos;
    let my_todos = move || {
        todo_list_state
            .get()
            .iter()
            .map(|item| (item.id, item.clone()))
            .collect::<Vec<_>>()
    };
    view! {
        <ul class="todo-list">
        <For
            each=my_todos
            key=|todo_key| todo_key.0
            children=move |item| {
                view! {
                    <li class="new-todo" > {item.1.content}
                        <button
                        class="remove"
                            on:click=move |_| {
                                set_todo_list_state.update(|todos| {
                                    todos.retain(|todo| &todo.id != &item.1.id)
                                });
                            }
                        >
                        </button>
                    </li>
                }
            }
        />
    </ul>
    }
}
```

Let’s examine the code above. The `<For/>` component has three important attributes:

*   `each`: Takes any function that returns an iterator, typically a signal or derived signal. If the iterator is not reactive, simply use the static rendering method instead of using the `<For/>` component
*   `key`: Specifies a unique and stable key for each row in the list. In the `<For />` component, we are just reading the `id` we already created initially when we added the to-do item to the list
*   `children`: Receives each item from the `each` iterator and returns a view. This is where you define the HTML markup for each item in the list. In our to-do list code, we render each to-do item's content and provide a `remove` button that triggers the deletion of the corresponding to-do item

It’s advisable to use unique `ids` for component `keys`. You could use the `rand` library function to generate random `ids`. Here’s an example function that uses the `rand` library to generate a new `id` and passes it to the function that creates new to-dos:

```rust
fn new_todo_id() -> u32 {
    let mut rng = rand::thread_rng();
    rng.gen()
}

let new_todo_item = TodoItem { id: new_todo_id(), content: input_value.clone() };
```

The output of the `TodoList` component code will look like so: ![Same Text Input Component Shown As In Previous Image, Now With List Of To Do Items Beneath](https://blog.logrocket.com/wp-content/uploads/2023/11/TodoList-component.png) That’s all there is to it. We now have a fully functional to-do application. You can find the [full code on GitHub](https://github.com/ezesundayeze/leptos-example). 

However, as you can see, our app doesn’t look very pretty. Of course, this is because there’s no CSS yet. Let’s take a look at how to add CSS to your Leptos application.

### Styling components in Leptos

Leptos has no opinion about [how you add CSS to your Leptos application](https://leptos-rs.github.io/leptos/interlude_styling.html). It allows you to use any strategy or CSS framework that you prefer. 

For example, you could add an inline style like so: `<input style="background:red"` `/>`. This should change the background of the input to red. 

You can also add your CSS to the `<head>` element using the `<style>` element in the `index.html` file. For example:

```html
<!DOCTYPE html>
<html>
  <meta charset="UTF-8">

<head>
  <style>
    .todo-list li:hover .remove {
      display: block;
    }
  </style>
</head>
<body></body>
</html>
```

Leptos also seamlessly integrates with Tailwind CSS, enabling you to utilize Tailwind's utility classes directly in your HTML markup. For example, to create a heading with the `p-6` and `text-4xl` classes, you would write:

```html
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"> Hello World!</h2>
```

In our example, we’ll keep it simple and just add the CSS in the `<head>` of the `index.html` file. There’s a lot of code, so I’ll strip most of it to show you a simple example:

```html
<!DOCTYPE html>
<html>
  <meta charset="UTF-8">
  <head>
    <style>
      html,
      body {
        font: 13px 'Arial', sans-serif;
        line-height: 1.5em;
        background: #a705a4;
        color: #4d4d4d;
        min-width: 399px;
        max-width: 799px;
        margin: 0 auto;
        font-weight: 250;
      }
    </style>
</head>
<body></body>
</html>
```

You can get the complete CSS we used to style the to-do app from [the GitHub repository](https://github.com/ezesundayeze/leptos-example/blob/master/index.html). 

Now that we've added the CSS, we're ready to replicate the demo to-do application we saw earlier. 

To run the app, simply execute the command `trunk serve --open`. This will compile the application and open it in your default browser on port 8080\. The output should resemble the following: ![](https://blog.logrocket.com/wp-content/uploads/2023/11/Final-app.png)

## Similar Rust frontend web frameworks

I've tried at least five different Rust frontend web frameworks, and in my opinion, Leptos stands out as an excellent choice. 

Leptos is relatively easy to learn and uses fine-grained reactivity, which sets it apart from frameworks like Dioxus. Focusing on fine-grain reactivity rather than the virtual DOM also aligns with the latest trends in frontend development and offers significant performance advantages. 

Here is a comparison table for similar Rust frontend web frameworks:

<table style="width: 100%">

<thead>

<tr>

<th style="width: 20%">Framework</th>

<th style="width: 16%">GitHub stars</th>

<th style="width: 16%">Virtual DOM</th>

<th style="width: 16%">Server-side rendering (SSR)</th>

<th style="width: 16%">Rendering method</th>

<th style="width: 16%">Architecture</th>

</tr>

</thead>

<tbody>

<tr>

<td>Dioxus</td>

<td>14.5K</td>

<td>Yes</td>

<td>Yes</td>

<td>HTML</td>

<td>React/Redux</td>

</tr>

<tr>

<td>Egui</td>

<td>17.1K</td>

<td>No</td>

<td>No</td>

<td>Canvas</td>

<td>ImGUI</td>

</tr>

<tr>

<td>Iced</td>

<td>21K</td>

<td>No</td>

<td>No</td>

<td>Canvas</td>

<td>TEA</td>

</tr>

<tr>

<td>Leptos</td>

<td>12.6K</td>

<td>No</td>

<td>Yes</td>

<td>HTML</td>

<td>FRP</td>

</tr>

<tr>

<td>MoonZoon</td>

<td>1.6K</td>

<td>No</td>

<td>No</td>

<td>HTML</td>

<td>FRP</td>

</tr>

<tr>

<td>Sauron</td>

<td>1.8K</td>

<td>No</td>

<td>Yes</td>

<td>HTML</td>

<td>FRP</td>

</tr>

<tr>

<td>Perseus</td>

<td>2K</td>

<td>No</td>

<td>Yes</td>

<td>HTML</td>

<td>FRP</td>

</tr>

</tbody>

</table>

## Conclusion

Leptos is an amazing Rust web frontend framework. It was built on the premise of scalability and making it a lot easier to build declarative UIs without sacrificing performance. 

We’ve really just scratched the surface of Leptos with this tutorial. There is a lot more you can do with Leptos — I recommend [visiting the Leptos documentation](http://leptos-rs.github.io/leptos/view/03_components.html) for more ideas. 

You can [check out the GitHub repository](https://github.com/ezesundayeze/leptos-example) that contains all the examples in this tutorial as a reference. If you have any questions, feel free to comment them below. 

Happy hacking.

---

## LogRocket: Full visibility into web frontends for Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, [try LogRocket](https://lp.logrocket.com/blg/signup). 

[![LogRocket Dashboard Free Trial Banner](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://lp.logrocket.com/blg/signup)

[LogRocket](https://logrocket.com/signup) is like a DVR for web apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — [start monitoring for free](https://lp.logrocket.com/blg/signup).