Time 0:35–1:15
In this lesson, you'll build a static screen with no state yet. When you're done, you'll see a header with a "Customers" title and a description inside a rounded card.
tutorial_261019/ # project root (run reflex commands here)
├── rxconfig.py # app name and plugin settings
├── pyproject.toml / uv.lock # Python dependencies (managed by uv)
├── assets/ # static files such as images → referenced like "/favicon.ico"
├── reflex.lock/ # frontend package lock files (commit these)
├── .web/ # compiled React code (generated, don't edit)
└── tutorial_261019/ # Python package with the same name as app_name
├── __init__.py
└── tutorial_261019.py # the file we'll work in (app = rx.App())Here's rxconfig.py. app_name must match the inner folder name, or Reflex won't find your app.
import reflex as rx
config = rx.Config(
app_name="tutorial_261019",
plugins=[
rx.plugins.SitemapPlugin(),
rx.plugins.TailwindV4Plugin(),
rx.plugins.RadixThemesPlugin(), # provides the default look for buttons, tables, etc.
],
)Delete everything in tutorial_261019/tutorial_261019.py and start with this:
import reflex as rx
def index() -> rx.Component:
return rx.heading("Customers")
app = rx.App()
app.add_page(index, title="Customer Data App")index() is a function that returns a piece of UI (a component). One page = one function.app = rx.App() must be at the top level of the module. Reflex looks for your app by this variable name.index, add_page(index) registers it at the root path /. title is the browser tab title.Check your browser — After you save, only the "Customers" heading is left in the top-left corner. It's normal for it to sit right against the edge, since there's no padding yet.
localhost:3000
Live previewReflex UI code really comes down to these two rules:
name=value, like size="6" or margin="0".rx.box( # <div>
rx.heading("Customers", size="6"), # child 1
rx.badge("2"), # child 2
display="flex", # style
gap="12px",
)align-items → align_items, margin-top → margin_top._: as → as_, type → type_.rx.color("slate", 11) is the Radix color palette. Higher numbers are darker, and the colors adapt to dark mode automatically. Use 1–2 for backgrounds, 4–6 for borders and 11–12 for text.Instructor note — Describing it as "HTML tags turned into function calls" makes it click quickly: rx.box = <div>, rx.text = <p>, rx.heading = <h1>–<h6>. Open the browser dev tools (F12) and show the tags that were actually generated.
The header has a left side (title + badge + description) and a right side (a button). The button comes in Lesson 4, so for now you'll build only the left side.
def index() -> rx.Component:
return rx.box(
rx.box(
rx.box(
rx.heading("Customers", size="6", as_="h3", margin="0"),
rx.badge("2", variant="soft", color_scheme="gray", radius="full"),
display="flex",
align_items="center",
gap="12px",
),
rx.text(
"Your customer directory and audience breakdown.",
size="2",
color=rx.color("slate", 11),
margin_top="6px",
),
),
display="flex",
align_items="center",
justify_content="space-between",
flex_wrap="wrap",
gap="16px",
padding="24px",
)"2" is a placeholder. In Lesson 3 you'll replace it with State.users.length() to show the real customer count.justify_content="space-between" pushes the children to opposite ends. That's why the button will sit at the far right.flex_wrap="wrap" moves the button to the next line on narrow screens (mobile support).Check your browser — You should see a gray rounded badge (2) next to the title, with a gray description underneath. There's no card border yet.
localhost:3000
Live previewYour customer directory and audience breakdown.
Wrap the header in one more rx.box to give it a card shape. This outer box becomes the frame for the whole app, and the table and chart will go inside it later.
def index() -> rx.Component:
return rx.box(
header, # the header box from 2-4
# the table (Lesson 3) and chart (Lesson 5) go here
width="100%",
min_width="0",
max_width="64em", # maximum card width
margin_x="auto", # center horizontally
margin_y="24px",
border=f"1px solid {rx.color('slate', 5)}",
border_radius="16px",
background=rx.color("slate", 1),
overflow="hidden", # keep content from spilling past the rounded corners
box_shadow="0 4px 24px rgba(0, 0, 0, 0.03)",
style={
"& *": {"box_sizing": "border-box"},
"& p, & h3": {"text_align": "left"},
},
)style={...} for CSS selectors that are hard to express as keyword arguments. "& *" means "every element inside this box."rx.color(...) inside an f-string. Just be careful to mix single and double quotes correctly.Check your browser — The header now sits inside a rounded card centered on the page. If you make the browser window narrower, the card shrinks with it.
localhost:3000
Live previewYour customer directory and audience breakdown.
border_radius to "4px" and then "32px" and compare the difference.rx.color_mode.button() to the header and check that the colors switch nicely in dark mode.← Previous
Lesson 1 · Meet Reflex and set up your environment
Next →
Lesson 3 · State and Vars: let data draw the screen
Get news about new lessons
We'll email you when this course gets new lessons or a follow-up course opens.
R
ReflexKR
Learn Python and Reflex by doing. Every example was run and verified on Reflex 0.10.0.
ReflexKR is free, with no banner ads. If it helped you, you can support it with a coffee. ☕ Buy us a coffee
Affiliate disclosure© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Questions & answers
Ask anything about this lesson. Questions from every language version of this page appear together.