Skip to content

rx.accordion.item inside rx.foreach renders without trigger styles (chevron drops below header) #6533

@jumbi77

Description

@jumbi77

Describe the bug

When rx.accordion.item is rendered through rx.foreach, the resulting accordion items are missing the styles defined in the add_style() methods of AccordionItem, AccordionHeader, AccordionTrigger and AccordionContent. Visible symptoms in the dynamic (foreach) version:

  1. The chevron drops onto a new line below the header content instead of staying on the right of the row (AccordionTrigger missing display: flex).
  2. There are no divider lines between accordion items (AccordionItem missing border-top).
  3. The expanded content of an item is flush against the left edge with no inner padding (AccordionContent missing padding-x and top/bottom spacers).

The same items rendered statically (without rx.foreach) render correctly.

To Reproduce

Run the code below. The top accordion uses rx.foreach, the bottom one lists the same items statically. All three symptoms above are visible side-by-side.

  • Code:
import reflex as rx


class BuchungenState(rx.State):
    buchungen: list[dict[str, str]] = [
        {"value": "kommt", "icon": "🙂", "label": "Clock In",
         "label_color": "green", "time_text": "9 minutes ago", "time_bg": "#2d5a3d",
         "booked_at": "01/14/2025 - 13:01:34", "open_since": "8:30"},
        {"value": "aussendienst", "icon": "🚧", "label": "Field Service Start",
         "label_color": "orange", "time_text": "8 minutes ago", "time_bg": "#7a3a1d",
         "booked_at": "01/14/2025 - 13:02:14", "open_since": "0:08"},
        {"value": "pause_anfang", "icon": "☕", "label": "Break Start",
         "label_color": "yellow", "time_text": "8 minutes ago", "time_bg": "#6b5a1d",
         "booked_at": "01/14/2025 - 13:02:45", "open_since": "0:08"},
        {"value": "pause_ende", "icon": "🍽️", "label": "Break End",
         "label_color": "yellow", "time_text": "2 minutes ago", "time_bg": "#6b5a1d",
         "booked_at": "01/14/2025 - 13:08:12", "open_since": "0:02"},
    ]


def buchung_item(b: dict) -> rx.Component:
    return rx.accordion.item(
        header=rx.hstack(
            rx.text(b["icon"], font_size="1.4em"),
            rx.badge(b["label"], color_scheme=b["label_color"], variant="outline", size="2"),
            rx.badge(b["time_text"], background=b["time_bg"], color="white", size="2"),
            align="center", spacing="3",
        ),
        content=rx.vstack(
            rx.text("Booked at: ", b["booked_at"], color="cyan"),
            rx.text("Open since: ", b["open_since"], color="cyan"),
            spacing="2", padding="0.5em 0",
        ),
        value=b["value"],
    )


def index() -> rx.Component:
    return rx.center(
        rx.vstack(
            # BROKEN: items rendered via rx.foreach
            rx.heading("Open Bookings (dynamic via rx.foreach)", size="6"),
            rx.accordion.root(
                rx.foreach(BuchungenState.buchungen, buchung_item),
                collapsible=True, type="multiple", variant="surface", width="100%",
            ),
            rx.divider(margin_y="2em"),
            # WORKS: same items, statically listed
            rx.heading("Open Bookings (static)", size="6"),
            rx.accordion.root(
                *[buchung_item(b) for b in BuchungenState.buchungen.default],
                collapsible=True, type="multiple", variant="surface", width="100%",
            ),
            spacing="4", padding="2em", align="center", width="100%", max_width="600px",
        ),
        width="100%", min_height="100vh", background="#1a1a1a",
    )


app = rx.App(theme=rx.theme(appearance="dark", accent_color="teal"))
app.add_page(index)

Expected behavior

Both accordions should render identically: each item with the header laid out as a single row (badges left, chevron right), thin divider lines between items, and the expanded body shown with inner padding so it doesn't touch the left edge of the item box. The static accordion already renders this way — the dynamic (foreach) one should match.

Screenshots

Image

Specifics (please complete the following information):

  • Python Version: 3.13
  • Reflex Version: reproduced on 0.8.22, 0.8.27, 0.9.1, 0.9.2, 0.9.2-post1
  • OS: macOS (also reproduces on Linux)
  • Browser (Optional): Firefox, Chromium (browser-independent)

Additional context

DOM diff between the two cases:

  • Static (works): the compiled JSX emits each accordion sub-element with an inline css={...} prop carrying its add_style() output. The rendered DOM gets Emotion-generated classes for AccordionItem, AccordionHeader, AccordionTrigger, AccordionContent.
  • Dynamic (broken): inside the foreach-generated JSX, the same sub-elements are emitted with only their bare class name (e.g. <button class="AccordionTrigger">) — no css prop, no Emotion class. No global CSS rule matches those class names, so each element falls back to the browser default rendering.

Suspected cause: the compile path used for components rendered inside rx.foreach appears to skip the per-instance add_style() output that the non-foreach path inlines as the css prop. This affects every primitive that relies on add_style() for its layout — Accordion is just where it's easiest to see. Other Radix primitives used inside rx.foreach are likely affected too.

Workaround: a global stylesheet that re-defines .AccordionItem, .AccordionHeader, .AccordionTrigger, .AccordionContent with the missing properties (copied from each class's add_style()) restores the layout, but obviously doesn't fix the underlying compile-path issue or the analogous issues for other primitives.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions