with statement: nesting in code mirrors nesting on screen. This page builds up from simple layout primitives to a complete interface, one layer at a time.
Composition controls the structure of your interface, while CSS handles styling and layout fine-tuning. Every component accepts a css_class prop that maps directly to Tailwind utility classes, so you can adjust widths, colors, spacing, and more without leaving Python.
Rows and columns
Every interface needs a way to arrange things on screen. Prefab provides two fundamental layout containers:Row arranges children horizontally, and Column stacks them vertically. Both accept a gap parameter that controls the spacing between children; the number maps to Tailwind’s spacing scale, so gap=4 means 1rem.
To place components inside a container, use Python’s with statement. Any component created inside a with block automatically becomes a child of that container.
Here’s a row of three green divs:
And here’s a column. By default, a Column stretches to fill the available width; the css_class="w-auto" override shrinks it to fit its content instead.
Nesting
Containers nest inside each other, and there’s no limit to how deep you can go. The structure of your Python code directly mirrors the structure of the rendered UI: each level of indentation corresponds to a level of visual nesting. This makes it easy to read code and know exactly what the output will look like. The blue divs sit in a horizontal row because they’re inside theRow block. The purple div sits below them because it’s a direct child of the outer Column. Each component attaches to whichever container is currently open; the innermost with block wins.
This is the core idea of Prefab composition: nesting containers. Every Prefab interface, from a simple form to a full dashboard, is built by nesting components inside other components.
Adding content
The layout patterns above work with any component. Prefab ships with a large library of prebuilt components: typography, badges, progress bars, inputs, charts, data tables, and many more. You can browse them all in the Playground. These components compose with Row and Column exactly the way the colored placeholders did. Prefab also includes layout components beyond these two, such as Grid, Dashboard, and Pages, for more complex arrangements; they appear later on this page or in their own docs. Here’s the same Column and Row pattern, with real content:Compound components
Some Prefab components are designed to be composed from sub-components. Card expectsCardHeader, CardContent, and CardFooter children. Tabs expects Tab children. Accordion expects AccordionItem children. The sub-components follow the same nesting model: each one opens a with block that collects its own children.
Cards are the most common compound component, wrapping related content in a bordered container with distinct header, content, and footer sections. The card below wraps the same status content from the previous example.
The nesting is deeper, but the principle hasn’t changed: every with block opens a container, and components created inside it become children.
Grids and control flow
When you need to arrange multiple items in a uniform layout, Grid places children in equal-width columns that wrap automatically. Passcolumns=3 for a three-column grid, or use min_column_width for responsive layouts that adapt to the available space.
Python loops
Since the DSL is plain Python, you can use afor loop to produce components from data. The loop runs once when the component tree is constructed, and the result is baked into the output: the renderer never sees the loop, just the components it produced. This works well when the data is static and known at build time.
ForEach
When the data lives in client-side state and can change dynamically, for example a list of items the user can add to or remove from, you need iteration that happens at render time.ForEach takes a state key pointing to a list and renders its children once per item, re-rendering automatically whenever the list changes.
Inside a ForEach block, {{ $item }} refers to the current item (or its fields directly if the item is a dict), and {{ $index }} gives the zero-based position. Because ForEach is a component in the tree (not a Python loop that runs and disappears), the renderer knows about the iteration. If an action appends a fourth task to the list, ForEach renders a new row for it automatically.
Conditionals
If, Elif, and Else provide render-time conditionals. If takes a boolean expression and only renders its children when the expression is truthy. Consecutive If/Elif/Else siblings form a single conditional chain: the renderer evaluates them in order and renders the first match; if nothing matches, the Else branch renders.
Change the select below to see the conditional switch between branches.
Build-time vs render-time
Prefab UIs are declarative: your Python code runs once to build a component tree, then the renderer takes over. Pythonfor loops and if statements produce a fixed result at build time. ForEach and If/Elif/Else are reactive: they live in the component tree and respond to state changes at render time. Use Python control flow for data that is fixed at build time, and use the control-flow components when the UI needs to respond to dynamic state. For any logic that needs to run after build time, use server actions.
Forward references
Sometimes you need a component’s reactive value before that component exists. Python executes top-to-bottom, so if a label above a slider needsslider.rx, you have a problem — the slider hasn’t been created at the point where the label is defined. Prefab offers two escape hatches: lambda Rx for reactive values, and defer/insert for structural rearrangement.
Lambda Rx
The common case is needing a component’s.rx before it’s created. Wrap the reference in a lambda, and Rx defers resolution until render time:
Rx(lambda: volume) doesn’t resolve right away — it captures the name volume and waits. When the lambda returns a stateful component, Rx extracts its .rx automatically, so Rx(lambda: volume) and Rx(lambda: volume.rx) are equivalent. Operations like / 100 and .percent() compose lazily too, so you can build entire expression chains against a component that doesn’t exist yet.
Structural rearrangement with defer/insert
Lambda Rx covers the common case of forward-referencing a value. Occasionally you need something different: building an entire component subtree outside the current tree and grafting it in later. That’s whatdefer() and insert() are for:
defer() block, auto-attachment is suspended — components can still nest children inside each other with with, they just won’t attach to the outer container. insert() grafts them in where you want them.
Most of the time you won’t need defer/insert. Lambda Rx covers the .rx forward-reference pattern, and normal composition handles everything else.