The Component Workshop: Reflex's 100 Library Components
·
Course overview
Layout
·
Box
·
Flex
·
Stack
·
Grid
·
Container
·
Section
·
Center
·
Spacer
·
Separator
·
Card
·
Inset
·
Aspect Ratio
·
Fragment
Typography
·
Text
·
Heading
·
Link
·
Code
·
Markdown
·
Blockquote
·
Quote
·
Strong
·
Em
·
Kbd
Forms
·
Button
·
Input
·
Text Area
·
Checkbox
·
Switch
·
Radio Group
·
Select
·
Slider
·
Form
·
Upload
Data display
·
Badge
·
Avatar
·
Icon
·
Callout
·
List
·
Data List
·
Code Block
·
Progress
·
Spinner
·
Scroll Area
·
Moment
Disclosure
·
Tabs
·
Accordion
·
Segmented Control
Overlay
·
Tooltip
·
Popover
·
Hover Card
·
Dialog
·
Alert Dialog
·
Drawer
·
Dropdown Menu
·
Context Menu
·
Toast
Dynamic rendering
·
Cond
·
Match
·
Foreach
·
Auto Scroll
Media
·
Image
·
Audio
·
Video
Tables and data grids
·
Table
·
Data Table
·
Data Editor
Charts
·
Area Chart
·
Bar Chart
·
Line Chart
·
Composed Chart
·
Pie Chart
·
Radar Chart
·
Radial Bar Chart
·
Scatter Chart
·
Funnel Chart
·
Treemap
·
Sankey Chart
·
Error Bar
Chart parts
·
Axis
·
Cartesian Grid
·
Chart Tooltip
·
Legend
·
Label
·
Reference
·
Brush
Other chart libraries
·
Plotly
·
Pyplot
HTML elements
·
HTML
·
HTML Text
·
HTML Layout
·
HTML Forms
·
HTML Media
·
HTML Tables
·
SVG
Other tools
·
Theme
·
Skeleton
·
Clipboard
·
Html Embed
·
Script
·
Memo
Workshop order
Without a design system, using plain HTML, style a small notice card with a close button and a link yourself.
rx.el contains the HTML elements of web development as they are. rx.el.div becomes <div>, rx.el.button becomes <button>.
Unlike the Radix-based rx.button, they have no look props such as variant or size, and they come completely unstyled.
This site has Tailwind's base reset (preflight) turned on, so even the button's border and the input's outline are wiped out.
A plain paragraph.
rx.el.div(
rx.el.p("A plain paragraph."),
rx.el.button("A plain button"),
rx.el.input(placeholder="A plain input"),
)You apply the look yourself with style props (keyword arguments named after CSS properties, with underscores). Change the values and watch how the bare button changes.
Try the props
label
background
color
padding
radius
weight
disabled
rx.el.button(
"Buy now",
background="#4f46e5",
color="white",
padding="8px 16px",
border_radius="6px",
font_weight="600",
disabled=False,
cursor="pointer",
)| Prop | Values | Default | Use |
|---|---|---|---|
CSS properties (padding, color …) | CSS value | none | Apply styles directly as keywords |
class_name | class string | none | CSS classes such as Tailwind |
id title hidden role tab_index lang dir | string, bool, int | none | HTML attributes common to all elements |
custom_attrs | dict | none | Attributes with a - in their name, such as aria-* and data-* |
type (button) | "submit" "reset" "button" | browser: "submit" | What a click does inside a form |
type (input) | "text" "email" "range" "date" … 22 kinds | "text" | Kind of input field |
value / on_change (input) | string, number / event | none | Controlled input. default_value is uncontrolled |
href target (a) | URL / "_blank" … | none | Link address and where it opens |
on_click on_key_down … | event handler | none | Basic events on every element |
Keyword style props, a style dictionary, and CSS classes in class_name. You can mix all three.
Use theme colors such as rx.color("indigo", 3) and the colors adapt properly in dark mode too.
rx.el.div(
rx.el.div(
"Style props",
padding="12px",
border=f"1px solid {rx.color('indigo', 7)}",
border_radius="8px",
background=rx.color("indigo", 3),
),
rx.el.div(
"style dict",
style={"padding": "12px", "border": "1px dashed gray", "border_radius": "8px"},
),
rx.el.div(
"Tailwind classes",
class_name="p-3 rounded-lg border-2 border-dotted font-semibold",
),
display="flex",
gap="12px",
flex_wrap="wrap",
)title is the browser's tooltip shown on hover, hidden hides the element, dir is the text direction and tab_index the Tab key order.
Attributes with a hyphen in their name, such as aria-label or data-kind, are passed through custom_attrs.
Right to left
rx.el.div(
rx.el.span("Hover me", title="A native tooltip", text_decoration="underline dotted"),
rx.el.span("You cannot see me", hidden=True),
rx.el.p("Right to left", dir="rtl", lang="en"),
rx.el.div(
"Focus me with Tab",
tab_index=0,
role="note",
custom_attrs={"aria-label": "Demo note", "data-kind": "demo"},
padding="8px",
border="1px solid gray",
border_radius="6px",
),
display="flex",
flex_direction="column",
gap="8px",
)Changing State with on_click works just as with rx.button. Inside a form, type matters.
If you don't set it, the browser default is "submit", so clicking submits the form immediately; write ordinary buttons as type="button".
name and value are sent along on submit, and form_* attributes such as form_action and form_method override the form's settings for that button only.
class HtmlElButtonState(rx.State):
count: int = 0
@rx.event
def add_one(self):
self.count += 1
rx.el.div(
rx.el.button(
"Add one",
type="button",
on_click=HtmlElButtonState.add_one,
padding="6px 14px",
border="1px solid gray",
border_radius="6px",
cursor="pointer",
),
rx.el.button("Disabled", disabled=True, padding="6px 14px", border="1px solid gray", border_radius="6px", opacity="0.5"),
rx.el.span("Count: ", HtmlElButtonState.count),
display="flex",
gap="12px",
align_items="center",
)Give it both value and on_change and it becomes a controlled input whose value the State holds. rx.el.input has its own on_change, on_key_down and on_key_up events.
on_key_down passes the name of the pressed key, so the example below shows the last key you pressed. type="range" turns it into a slider.
Hello, (last key: )
Level: 40
class HtmlElInputState(rx.State):
name: str = ""
level: int = 40
last_key: str = ""
@rx.event
def change_name(self, value: str):
self.name = value
@rx.event
def change_level(self, value: float):
self.level = int(value)
@rx.event
def remember_key(self, key: str):
self.last_key = key
rx.el.div(
rx.el.input(
type="text",
value=HtmlElInputState.name,
on_change=HtmlElInputState.change_name,
on_key_down=HtmlElInputState.remember_key,
placeholder="Your name",
max_length=20,
padding="6px 10px",
border="1px solid gray",
border_radius="6px",
),
rx.el.p("Hello, ", HtmlElInputState.name, " (last key: ", HtmlElInputState.last_key, ")"),
rx.el.input(
type="range",
min=0,
max=100,
step=5,
value=HtmlElInputState.level,
on_change=HtmlElInputState.change_level,
),
rx.el.p("Level: ", HtmlElInputState.level),
display="flex",
flex_direction="column",
gap="8px",
max_width="320px",
)A single type turns it into a date, color, checkbox, email or number field. To give just an initial value without State, use default_value or default_checked.
Validation attributes such as required, min/max, pattern and max_length are checked by the browser when the form is submitted.
rx.el.div(
rx.el.input(type="date"),
rx.el.input(type="color", default_value="#6e56cf"),
rx.el.label(rx.el.input(type="checkbox", default_checked=True), " Remember me"),
rx.el.input(type="email", placeholder="you@example.com", required=True, border="1px solid gray", padding="4px 8px"),
rx.el.input(type="number", min=1, max=9, default_value=3, border="1px solid gray", padding="4px 8px", width="80px"),
display="flex",
gap="12px",
flex_wrap="wrap",
align_items="center",
)p is a paragraph and takes up a whole line; span is a fragment that stays within a line of text. div is a box with no meaning, for grouping anything.
A p is a block. A span stays inside the line.
rx.el.div(
rx.el.p(
"A p is a block. A ",
rx.el.span("span", color=rx.color("tomato", 11), font_weight="bold"),
" stays inside the line.",
),
rx.el.div("A div is a box with no meaning.", padding="8px", background=rx.color("gray", 3)),
)In Reflex 0.10, rx.el.a is rendered as React Router's Link, and href is moved to to internally.
So in-site addresses such as "/" navigate without reloading the page, while external addresses open like ordinary links.
To navigate with a full reload, use reload_document=True; to replace the history entry, replace=True; for a transition effect, view_transition=True. download makes the browser download the target as a file.
rx.el.div(
rx.el.a(
"Reflex docs (new tab)",
href="https://reflex.dev/docs/library/html/html/",
target="_blank",
rel="noopener noreferrer",
color=rx.color("indigo", 11),
text_decoration="underline",
),
rx.el.a("Home page", href="/", color=rx.color("indigo", 11), text_decoration="underline"),
display="flex",
gap="16px",
)rx.el has more than 100 elements. Text elements, document structure, forms, media, tables and SVG are covered group by group on the next five pages.
The badge is a span, the body a p, the actions an a and a button, all styled with style props. Closing it makes the State hide the card.
class HtmlElNoticeState(rx.State):
visible: bool = True
@rx.event
def dismiss(self):
self.visible = False
@rx.event
def show_again(self):
self.visible = True
rx.cond(
HtmlElNoticeState.visible,
rx.el.div(
rx.el.p(
rx.el.span(
"New",
background=rx.color("indigo", 9),
color="white",
padding="2px 8px",
border_radius="999px",
font_size="12px",
margin_right="8px",
),
"Version 2 is out.",
font_weight="600",
),
rx.el.p("Read what changed before you update.", color=rx.color("gray", 11), margin_y="6px"),
rx.el.div(
rx.el.a(
"Release notes",
href="https://reflex.dev/docs/",
target="_blank",
rel="noopener noreferrer",
color=rx.color("indigo", 11),
text_decoration="underline",
),
rx.el.button(
"Dismiss",
type="button",
on_click=HtmlElNoticeState.dismiss,
padding="4px 12px",
border=f"1px solid {rx.color('gray', 7)}",
border_radius="6px",
cursor="pointer",
),
display="flex",
gap="16px",
align_items="center",
),
padding="16px",
border=f"1px solid {rx.color('indigo', 6)}",
border_radius="10px",
background=rx.color("indigo", 2),
max_width="360px",
),
rx.el.button("Show again", on_click=HtmlElNoticeState.show_again, text_decoration="underline", cursor="pointer"),
)Workshop notes
rx.el.<tag> is the plain HTML element, with no styling. Apply the look with CSS keyword props, style or class_name.
title, hidden, dir, tab_index and role are common to all elements. Pass aria-* and data-* via custom_attrs.
An rx.el.button inside a form defaults to submit. Ordinary buttons use type="button".
rx.el.input is controlled with value + on_change, uncontrolled with default_value. on_key_down passes the pressed key.
In 0.10, rx.el.a is a React Router Link: href becomes to, so in-site links navigate without reloading.
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.