R

ReflexKR

Reflex Docs

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

The Component Workshop: Reflex's 100 Library Components
HTML elements

HTML Forms

rx.el.fieldsetrx.el.formrx.el.labelrx.el.legendrx.el.meterrx.el.optgrouprx.el.optionrx.el.outputrx.el.progressrx.el.selectrx.el.textarea

Official docs

Workshop order

A café order form: drinks from a list grouped by type, the pickup method as radio buttons, requests as multi-line text, and a summary once the order is placed.

These are the form-related HTML elements: form, which collects inputs; the grouping box fieldset and its title legend; the pickers select, optgroup and option; the multi-line input textarea; and meter, progress and output, which display values. They are lighter than the Radix form components (rx.select, rx.text_area) and use the behavior the browser provides out of the box.

rx.el.label(
    "Size ",
    rx.el.select(
        rx.el.option("Small", value="s"),
        rx.el.option("Medium", value="m"),
        rx.el.option("Large", value="l"),
        default_value="m",
        border="1px solid gray",
        padding="2px 6px",
    ),
)

Try the props

meter changes color depending on whether the value is in a good or a bad range. low and high are the range boundaries, and optimum decides which side is good.

Try the props

value

65

low

30

high

80

optimum

90

rx.el.div(
    rx.el.meter(value=65, min=0, max=100, low=30, high=80, optimum=90, width="260px", height="20px"),
    rx.el.progress(value=65, max=100, width="260px"),
    display="flex",
    flex_direction="column",
    gap="10px",
)
If optimum is below low, low values count as good; if it is above high, high values count as good. progress is just a progress bar and never changes color.

Try the props

placeholder

rows

resize

disabled

read_only

rx.el.textarea(
    placeholder="Write a note",
    rows="4",
    resize="vertical",
    disabled=False,
    read_only=False,
    border="1px solid gray",
    border_radius="6px",
    padding="6px 8px",
    width="280px",
)
rows is the number of visible lines. resize is a CSS property that decides whether the user can drag the corner to resize it.
PropValuesDefaultUse
on_submit (form)event handlernone (only prevents the default submit)Receives a dict of values keyed by name
reset_on_submit (form)True / FalseFalseClears the fields after submitting
no_validate (form)True / FalseFalseTurns off the browser's required and pattern checks
html_for (label, output)element idnoneThe input to connect. Clicking the label moves to that field
disabled (fieldset, select, option, optgroup)True / FalseFalseDisables every input inside
value / on_change (select, textarea)string / eventnoneControlled input. default_value is uncontrolled
multiple size (select)bool / integerFalse / nonePick several, number of visible rows
label (optgroup)stringnoneGroup title
rows cols max_length (textarea)integernoneSize and character limit
auto_height enter_key_submit (textarea)True / FalseFalseGrow with the text, submit with Enter
value min max low high optimum (meter)numbermin=0 max=1A measurement within a range
value max (progress)numbermax=1How far along a task is. Without value, shows an in-progress state

rx.el.form and on_submit

When the submit button is clicked, on_submit passes the values of the inputs in the form as a dict keyed by name. A checkbox is included, as "on", only when it is checked. Even without on_submit, rx.el.form prevents the browser's default submit (a page reload). With reset_on_submit=True, the fields are cleared after submitting.

    class HtmlFormSignupState(rx.State):
        submitted: dict[str, str] = {}
    
        @rx.event
        def handle_submit(self, form_data: dict):
            self.submitted = {key: str(value) for key, value in form_data.items()}
    
    
    rx.el.div(
        rx.el.form(
            rx.el.input(name="email", type="email", placeholder="Email", required=True, border="1px solid gray", padding="4px 8px"),
            rx.el.select(
                rx.el.option("Weekly", value="weekly"),
                rx.el.option("Monthly", value="monthly"),
                name="plan",
                border="1px solid gray",
                padding="4px",
            ),
            rx.el.label(rx.el.input(type="checkbox", name="news"), " Send news"),
            rx.el.button("Sign up", type="submit", padding="4px 12px", border="1px solid gray", border_radius="6px"),
            on_submit=HtmlFormSignupState.handle_submit,
            reset_on_submit=True,
            display="flex",
            gap="8px",
            flex_wrap="wrap",
            align_items="center",
        ),
        rx.el.ul(
            rx.foreach(HtmlFormSignupState.submitted, lambda kv: rx.el.li(rx.el.code(kv[0]), " = ", kv[1])),
            margin_top="10px",
        ),
    )

    fieldset, legend, label

    fieldset groups related inputs inside a border, and legend is the group's title. Give fieldset disabled=True and every input inside is disabled. label connects to an input either by wrapping it or by putting the input's id in html_for. Clicking a connected label moves the cursor into the input.

    Delivery
    Gift wrap (sold out)
    rx.el.div(
        rx.el.fieldset(
            rx.el.legend("Delivery", font_weight="bold"),
            rx.el.label(rx.el.input(type="radio", name="ship", value="pickup", default_checked=True), " Pickup"),
            rx.el.label(rx.el.input(type="radio", name="ship", value="courier"), " Courier"),
            border="1px solid gray",
            border_radius="6px",
            padding="8px 12px",
            display="flex",
            gap="12px",
        ),
        rx.el.fieldset(
            rx.el.legend("Gift wrap (sold out)", font_weight="bold"),
            rx.el.label(rx.el.input(type="checkbox"), " Wrap it"),
            rx.el.label("Message ", rx.el.input(id="htmlforms_gift_msg", border="1px solid gray")),
            disabled=True,
            border="1px solid gray",
            border_radius="6px",
            padding="8px 12px",
            display="flex",
            gap="12px",
            opacity="0.6",
        ),
        rx.el.label("Coupon", html_for="htmlforms_coupon", margin_right="8px"),
        rx.el.input(id="htmlforms_coupon", placeholder="Click the label", border="1px solid gray", padding="2px 6px"),
        display="flex",
        flex_direction="column",
        gap="10px",
        align_items="start",
    )

    select, optgroup, option

    option is an item to pick, and optgroup groups items under a label title. Items marked disabled can't be picked. With value and on_change, the State holds the chosen value. Add multiple=True and size and you pick several items from an open list with Ctrl or Shift.

    You picked: soup

    class HtmlFormSelectState(rx.State):
        dish: str = "soup"
    
        @rx.event
        def change_dish(self, value: str):
            self.dish = value
    
    
    rx.el.div(
        rx.el.select(
            rx.el.optgroup(
                rx.el.option("Soup", value="soup"),
                rx.el.option("Salad", value="salad"),
                label="Starters",
            ),
            rx.el.optgroup(
                rx.el.option("Pasta", value="pasta"),
                rx.el.option("Steak (sold out)", value="steak", disabled=True),
                label="Mains",
            ),
            value=HtmlFormSelectState.dish,
            on_change=HtmlFormSelectState.change_dish,
            border="1px solid gray",
            padding="4px",
        ),
        rx.el.p("You picked: ", rx.el.strong(HtmlFormSelectState.dish)),
        rx.el.select(
            rx.el.option("Red"),
            rx.el.option("Green"),
            rx.el.option("Blue"),
            rx.el.option("Black"),
            multiple=True,
            size=3,
            border="1px solid gray",
            width="120px",
        ),
        display="flex",
        flex_direction="column",
        gap="8px",
        align_items="start",
    )

    textarea: sending with Enter and automatic height

    Reflex's rx.el.textarea has two props that HTML doesn't. With enter_key_submit=True, Enter submits the form and Shift+Enter starts a new line. With auto_height=True, the height grows as the text gets longer (use it together with min_height). enter_key_submit can't be combined with on_key_down.

      class HtmlFormChatState(rx.State):
          messages: list[str] = []
      
          @rx.event
          def send(self, form_data: dict):
              text = str(form_data.get("message", "")).strip()
              if text:
                  self.messages = [*self.messages, text][-4:]
      
      
      rx.el.div(
          rx.el.ul(
              rx.foreach(HtmlFormChatState.messages, lambda m: rx.el.li(m, white_space="pre-wrap")),
              list_style_type="disc",
              padding_left="1.5em",
          ),
          rx.el.form(
              rx.el.textarea(
                  name="message",
                  placeholder="Type and press Enter",
                  enter_key_submit=True,
                  auto_height=True,
                  max_length=200,
                  min_height="40px",
                  width="100%",
                  border="1px solid gray",
                  border_radius="6px",
                  padding="6px 8px",
              ),
              on_submit=HtmlFormChatState.send,
              reset_on_submit=True,
          ),
          max_width="360px",
      )

      output, meter, progress

      output is a place to show a calculated result; put the ids of the inputs used in the calculation in html_for, separated by spaces. meter is a measurement with a fixed range, such as disk usage, and progress is how far along a task is. Without a value, progress shows an "in progress" state.

      + = 5

      class HtmlFormOutputState(rx.State):
          a: int = 2
          b: int = 3
      
          @rx.event
          def change_a(self, value: float):
              self.a = int(value)
      
          @rx.event
          def change_b(self, value: float):
              self.b = int(value)
      
          @rx.var
          def total(self) -> int:
              return self.a + self.b
      
      
      rx.el.div(
          rx.el.p(
              rx.el.input(id="htmlforms_a", type="number", value=HtmlFormOutputState.a, on_change=HtmlFormOutputState.change_a, width="60px", border="1px solid gray"),
              " + ",
              rx.el.input(id="htmlforms_b", type="number", value=HtmlFormOutputState.b, on_change=HtmlFormOutputState.change_b, width="60px", border="1px solid gray"),
              " = ",
              rx.el.output(HtmlFormOutputState.total, html_for="htmlforms_a htmlforms_b", name="total", font_weight="bold"),
          ),
          rx.el.label("Disk ", rx.el.meter(value=0.7, low=0.5, high=0.8, optimum=0.2, width="160px")),
          rx.el.label("Loading ", rx.el.progress(width="160px")),
          display="flex",
          flex_direction="column",
          gap="8px",
      )

      Filling the order

      Drinks are grouped with optgroup, the quantity is a number input, the pickup method is radio buttons inside a fieldset, and the requests are a textarea. On submit, the State writes a summary into output.

      Your order
      Pickup
      class HtmlFormOrderState(rx.State):
          summary: str = ""
      
          @rx.event
          def place_order(self, form_data: dict):
              qty = form_data.get("qty") or "1"
              note = str(form_data.get("note", "")).strip() or "none"
              self.summary = f"{qty} x {form_data.get('drink')} ({form_data.get('size')}), {form_data.get('pickup')}. Note: {note}"
      
      
      rx.el.div(
          rx.el.form(
              rx.el.fieldset(
                  rx.el.legend("Your order", font_weight="bold"),
                  rx.el.select(
                      rx.el.optgroup(rx.el.option("Latte"), rx.el.option("Mocha"), label="Coffee"),
                      rx.el.optgroup(rx.el.option("Green tea"), rx.el.option("Chai"), label="Tea"),
                      name="drink",
                      border="1px solid gray",
                      padding="4px",
                  ),
                  rx.el.select(rx.el.option("Small"), rx.el.option("Large"), name="size", border="1px solid gray", padding="4px"),
                  rx.el.input(name="qty", type="number", min=1, max=9, default_value=1, width="60px", border="1px solid gray", padding="4px"),
                  display="flex",
                  gap="8px",
                  flex_wrap="wrap",
                  border="1px solid gray",
                  border_radius="6px",
                  padding="8px 12px",
              ),
              rx.el.fieldset(
                  rx.el.legend("Pickup", font_weight="bold"),
                  rx.el.label(rx.el.input(type="radio", name="pickup", value="for here", default_checked=True), " For here"),
                  rx.el.label(rx.el.input(type="radio", name="pickup", value="to go"), " To go"),
                  display="flex",
                  gap="12px",
                  border="1px solid gray",
                  border_radius="6px",
                  padding="8px 12px",
              ),
              rx.el.textarea(name="note", placeholder="Notes for the barista", rows=2, border="1px solid gray", border_radius="6px", padding="6px 8px"),
              rx.el.button("Order", type="submit", padding="6px 14px", border="1px solid gray", border_radius="6px", background=rx.color("indigo", 3)),
              on_submit=HtmlFormOrderState.place_order,
              display="flex",
              flex_direction="column",
              gap="10px",
          ),
          rx.el.output(HtmlFormOrderState.summary, display="block", margin_top="10px", font_weight="600"),
          max_width="420px",
      )

      Workshop notes

      • on_submit of rx.el.form receives a {name: value} dict. Give every input a name.

      • Group with fieldset + legend, and disable them all at once with fieldset's disabled. Connect label to an id with html_for.

      • select uses option and optgroup and is controlled with value + on_change. multiple and size allow several picks.

      • enter_key_submit and auto_height on textarea are convenience props added by Reflex.

      • meter is a measurement whose color changes with low, high and optimum; progress shows progress; output shows a calculated result.

      ← Previous

      HTML elements · HTML Layout

      Next →

      HTML elements · HTML Media

      Get news about new lessons

      We'll email you when this course gets new lessons or a follow-up course opens.

      We collect: your email address · Purpose: news about new lessons or the launch of this course · Kept until: you ask us to remove it. We never use it for anything else or share it. Reply to any notice email and we'll delete it right away.

      Questions & answers

      Ask anything about this lesson. Questions from every language version of this page appear together.

      Shown as plain text, without links or formatting.

      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
      한국어English日本語简体中文EspañolDeutsch

      © 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

      Built with Reflex