For AI agents: the complete documentation index is at llms.txt. Markdown versions are available by appending .md or sending Accept: text/markdown.
Reflex Logo
Docs Logo
Getting Started

/

Dashboard Tutorial

Data Dashboard

~20 min hands-on · Build a small data dashboard where users can input data that renders in a table and a graph.

This tutorial does not assume any existing Reflex knowledge, but we do recommend checking out the quick Basics Guide first. The techniques you'll learn are fundamental to any Reflex app.

This tutorial is divided into several sections:

  • Setup: Get your machine ready.
  • Overview: Components and props.
  • Dynamic data with State: Render data that changes.
  • Add data with a form: Forms + event handlers.
  • Plot a graph: Reflex's graphing components.
  • Customize + Full app: Customize and see the finished code.

What are you building?

An interactive data dashboard: a table of users, a form to add more, and a bar chart that updates as data changes. Want to skip ahead? Jump to the Full app at the bottom.

Users

Add customers and watch the chart update.

NameEmailGender
Danilo Sousa[email protected]Male
Zahra Ambessa[email protected]Female

Setup

  1. Install Reflex if you haven't already.
  2. Create a folder called dashboard_tutorial and cd into it.
  3. Run uv init and uv add reflex.
  4. Run uv run reflex init and choose template 0 (the blank template).
  5. Run uv run reflex run to start the app and confirm everything works.

Overview

Starter code

The reflex init command scaffolds an rxconfig.py (app config), an assets/ folder for static files, and a dashboard_tutorial/dashboard_tutorial.py module containing your app. Open that module and replace its contents — we'll build the app up from scratch.

A minimal Reflex page is just a component function plus an app that registers it:

For the rest of the tutorial the app = rx.App() and app.add_page lines are implied and not shown — we'll come back to them in Customize.

Create a table

The rx.table component has a root that wraps a header and a body. The header takes rowcolumn_header_cell components; the body takes rowcell components holding the actual data. Props like variant and size customize the look:

NameEmailGender
Danilo Sousa[email protected]Male
Zahra Ambessa[email protected]Female
Expand

Dynamic data with State

The table above is static — the rows are hardcoded. To make it dynamic, we move the data onto state: a Python class whose fields (state vars) hold the app's data and whose methods (event handlers) mutate them.

We'll model each row as a User dataclass so we can access fields by name (user.name) instead of by index:

To iterate a list state var, use — it takes an iterable and a function that renders each item. Here show_user receives a User and returns a table.row:

Expand

NameEmailGender
Danilo Sousa[email protected]Male
Zahra Ambessa[email protected]Female

The table looks the same, but the rows now come from state — next we'll add a form that appends to State.users so new rows appear automatically.

Add data with a form

We build a form using rx.form, which takes several components such as rx.input and rx.select, which represent the form fields that allow you to add information to submit with the form. Check out the form docs for more information on form components.

The rx.input component takes in several props. The placeholder prop is the text that is displayed in the input field when it is empty. The name prop is the name of the input field, which gets passed through in the dictionary when the form is submitted. The required prop is a boolean that determines if the input field is required.

The rx.select component takes in a list of options that are displayed in the dropdown. The other props used here are identical to the rx.input component.

This form is all very compact as you can see from the example, so we need to add some styling to make it look better. We can do this by adding a vstack component around the form fields. The vstack component stacks the form fields vertically. Check out the layout docs for more information on how to layout your app.

Now you have probably realised that we have all the form fields, but we have no way to submit the form. We can add a submit button to the form by adding a rx.button component to the vstack component. The rx.button component takes in the text that is displayed on the button and the type prop which is the type of button. The type prop is set to submit so that the form is submitted when the button is clicked.

In addition to this we need a way to update the users state variable when the form is submitted. All state changes are handled through functions in the state class, called event handlers.

Components have special props called event triggers, such as on_submit, that can be used to make components interactive. Event triggers connect components to event handlers, which update the state. Different event triggers expect the event handler that you hook them up to, to take in different arguments (and some do not take in any arguments).

The on_submit event trigger of rx.form is hooked up to the add_user event handler that is defined in the State class. This event trigger expects to pass a dict, containing the form data, to the event handler that it is hooked up to. The add_user event handler takes in the form data as a dictionary and appends it to the users state variable.

Expand

Finally we must add the new form() component we have defined to the index() function so that the form is rendered on the page.

Below is the full code for the app so far. If you try this form out you will see that you can add new users to the table by filling out the form and clicking the submit button. The form data will also appear as a toast (a small window in the corner of the page) on the screen when submitted.

NameEmailGender
Danilo Sousa[email protected]Male
Zahra Ambessa[email protected]Female
Expand

Put the form in a dialog

In Reflex, we like to make the user interaction as intuitive as possible. Placing the form we just constructed in an overlay creates a focused interaction by dimming the background, and ensures a cleaner layout when you have multiple action points such as editing and deleting as well.

We will place the form inside of a rx.dialog component (also called a modal). The rx.dialog.root contains all the parts of a dialog, and the rx.dialog.trigger wraps the control that will open the dialog. In our case the trigger will be an rx.button that says "Add User" as shown below.

After the trigger we have the rx.dialog.content which contains everything within our dialog, including a title, a description and our form. The first way to close the dialog is without submitting the form and the second way is to close the dialog by submitting the form as shown below. This requires two rx.dialog.close components within the dialog.

The total code for the dialog with the form in it is below.

Expand

At this point we have an app that allows you to add users to a table by filling out a form. The form is placed in a dialog that can be opened by clicking the "Add User" button. We change the name of the component from form to add_customer_button and update this in our index component. The full app so far and code are below.

NameEmailGender
Danilo Sousa[email protected]Male
Zahra Ambessa[email protected]Female
Expand

Plot a graph

Next we'll plot the user data in a graph using Reflex's built-in recharts library, counting users by gender.

Transform the data

The graphing components in Reflex expect to take in a list of dictionaries. Each dictionary represents a data point on the graph and contains the x and y values. We will create a new event handler in the state called transform_data to transform the user data into the format that the graphing components expect. We must also create a new state variable called users_for_graph to store the transformed data, which will be used to render the graph.

Expand

As we can see above the transform_data event handler uses the Counter class from the collections module to count the number of users of each gender. We then create a list of dictionaries from this which we set to the state var users_for_graph.

Finally we can see that whenever we add a new user through submitting the form and running the add_user event handler, we call the transform_data event handler to update the users_for_graph state variable.

Render the graph

We use the rx.recharts.bar_chart component to render the graph. We pass through the state variable for our graphing data as data=State.users_for_graph. We also pass in a rx.recharts.bar component which represents the bars on the graph. The rx.recharts.bar component takes in the data_key prop which is the key in the data dictionary that represents the y value of the bar. The stroke and fill props are used to set the color of the bars.

The rx.recharts.bar_chart component also takes in rx.recharts.x_axis and rx.recharts.y_axis components which represent the x and y axes of the graph. The data_key prop of the rx.recharts.x_axis component is set to the key in the data dictionary that represents the x value of the bar. Finally we add width and height props to set the size of the graph.

Finally we add this graph() component to our index() component so that the graph is rendered on the page. The code for the full app with the graph included is below. If you try this out you will see that the graph updates whenever you add a new user to the table.

NameEmailGender
Danilo Sousa[email protected]Male
Zahra Ambessa[email protected]Female
Expand

If you run the app locally with no seed users, the graph is empty until you add one — transform_data only runs when a user is added. The next section fixes that by calling it on page load.

Customize

Revisit app.add_page

At the beginning of this tutorial we mentioned that the app.add_page function is required for every Reflex app. This function is used to add a component to a page.

The app.add_page currently looks like this app.add_page(index). We could change the route that the page renders on by setting the route prop such as route="/custom-route", this would change the route to http://localhost:3000/custom-route for this page.

We can also set a title to be shown in the browser tab and a description as shown in search results.

To solve the problem we had above about our graph not loading when the page loads, we can use on_load inside of app.add_page to call the transform_data event handler when the page loads. This would look like on_load=State.transform_data. Below see what our app.add_page would look like with some of the changes above added.

NameEmailGender
Danilo Sousa[email protected]Male
Zahra Ambessa[email protected]Female

Revisit rx.App()

At the beginning of the tutorial we also mentioned that we defined our app using app=rx.App(). We can also pass in some props to the rx.App component to customize the app.

The most important one is theme which allows you to customize the look and feel of the app. The theme prop takes in an rx.theme component which has several props that can be set.

The radius prop sets the global radius value for the app that is inherited by all components that have a radius prop. It can be overwritten locally for a specific component by manually setting the radius prop.

The accent_color prop sets the accent color of the app. See the theme docs for the full list of options.

To see other props that can be set at the app level check out this documentation

The theme applies at the app level, so you'll need to run locally to see it in action.

Full app styled

Finally let's make some styling updates. We will add hover styling to the table rows and center the table inside show_user with style={"_hover": {"bg": rx.color("gray", 3)}}, align="center".

In addition, we will add some width="100%" and align="center" to the index() component to center the items on the page and ensure they stretch the full width of the page.

Check out the full code and interactive app below:

Users

Add customers and watch the chart update.

NameEmailGender
Danilo Sousa[email protected]Male
Zahra Ambessa[email protected]Female
Expand

Recap

You built:

  • A table that displays user data.
  • A form (inside a dialog) to add new users.
  • A bar chart that visualizes the distribution.

Along the way you learned:

  • State — how to store data that changes over time.
  • Events — how to respond to user actions and update the UI.
  • Styling — tweaking theme, layout, and hover states.
Built with Reflex