20 KiB
Layout Component
Introduction
The Layout component provides a complete application structure with fixed header and footer, a scrollable main content area, and optional collapsible side drawers. It's designed to be the foundation of your FastHTML application's UI.
Key features:
- Fixed header and footer that stay visible while scrolling
- Collapsible left and right drawers for navigation, tools, or auxiliary content
- Resizable drawers with drag handles
- Automatic state persistence per session
- Single instance per session (singleton pattern)
Common use cases:
- Application with navigation sidebar
- Dashboard with tools panel
- Admin interface with settings drawer
- Documentation site with table of contents
Quick Start
Here's a minimal example showing an application with a navigation sidebar:
from fasthtml.common import *
from myfasthtml.controls.Layout import Layout
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
# Create the layout instance
layout = Layout(parent=root_instance, app_name="My App")
# Add navigation items to the left drawer
layout.left_drawer.add(
mk.mk(Div("Home"), command=Command(...))
)
layout.left_drawer.add(
mk.mk(Div("About"), command=Command(...))
)
layout.left_drawer.add(
mk.mk(Div("Contact"), command=Command(...))
)
# Set the main content
layout.set_main(
Div(
H1("Welcome"),
P("This is the main content area")
)
)
# Render the layout
return layout
This creates a complete application layout with:
- A header displaying the app name and drawer toggle button
- A collapsible left drawer with interactive navigation items
- A main content area that updates when navigation items are clicked
- An empty footer
Note: Navigation items use commands to update the main content area without page reload. See the Commands section below for details.
Basic Usage
Creating a Layout
The Layout component is a SingleInstance, meaning there's only one instance per session. Create it by providing a
parent instance and an application name:
layout = Layout(parent=root_instance, app_name="My Application")
Content Zones
The Layout provides six content zones where you can add components:
┌──────────────────────────────────────────────────────────┐
│ Header │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ header_left │ │ header_right │ │
│ └─────────────────┘ └─────────────────┘ │
├─────────┬────────────────────────────────────┬───────────┤
│ │ │ │
│ left │ │ right │
│ drawer │ Main Content │ drawer │
│ │ │ │
│ │ │ │
├─────────┴────────────────────────────────────┴───────────┤
│ Footer │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ footer_left │ │ footer_right │ │
│ └─────────────────┘ └─────────────────┘ │
└──────────────────────────────────────────────────────────┘
Zone details:
| Zone | Typical Use |
|---|---|
header_left |
App logo, menu button, breadcrumbs |
header_right |
User profile, notifications, settings |
left_drawer |
Navigation menu, tree view, filters |
right_drawer |
Tools panel, properties inspector, debug info |
footer_left |
Copyright, legal links, version |
footer_right |
Status indicators, connection state |
Adding Content to Zones
Use the .add() method to add components to any zone:
# Header
layout.header_left.add(Div("Logo"))
layout.header_right.add(Div("User: Admin"))
# Drawers
layout.left_drawer.add(Div("Navigation"))
layout.right_drawer.add(Div("Tools"))
# Footer
layout.footer_left.add(Div("© 2024 My App"))
layout.footer_right.add(Div("v1.0.0"))
Setting Main Content
The main content area displays your page content and can be updated dynamically:
# Set initial content
layout.set_main(
Div(
H1("Dashboard"),
P("Welcome to your dashboard")
)
)
# Update later (typically via commands)
layout.set_main(
Div(
H1("Settings"),
P("Configure your preferences")
)
)
Controlling Drawers
By default, both drawers are visible. The drawer state is managed automatically:
- Users can toggle drawers using the icon buttons in the header
- Users can resize drawers by dragging the handle
- Drawer state persists within the session
The initial drawer widths are:
- Left drawer: 250px
- Right drawer: 250px
These can be adjusted by users and the state is preserved automatically.
Content System
Understanding Groups
Each content zone (header_left, header_right, drawers, footer) supports groups to organize related items. Groups are separated visually by dividers and can have optional labels.
Adding Content to Groups
When adding content, you can optionally specify a group name:
# Add items to different groups in the left drawer
layout.left_drawer.add(Div("Dashboard"), group="main")
layout.left_drawer.add(Div("Analytics"), group="main")
layout.left_drawer.add(Div("Settings"), group="preferences")
layout.left_drawer.add(Div("Profile"), group="preferences")
This creates two groups:
- main: Dashboard, Analytics
- preferences: Settings, Profile
A visual divider automatically appears between groups.
Custom Group Labels
You can provide a custom FastHTML element to display as the group header:
# Add a styled group header
layout.left_drawer.add_group(
"Navigation",
group_ft=Div("MAIN MENU", cls="font-bold text-sm opacity-60 px-2 py-1")
)
# Then add items to this group
layout.left_drawer.add(Div("Home"), group="Navigation")
layout.left_drawer.add(Div("About"), group="Navigation")
Ungrouped Content
If you don't specify a group, content is added to the default (None) group:
# These items are in the default group
layout.left_drawer.add(Div("Quick Action 1"))
layout.left_drawer.add(Div("Quick Action 2"))
Preventing Duplicates
The Content system automatically prevents adding duplicate items based on their id attribute:
item = Div("Unique Item", id="my-item")
layout.left_drawer.add(item)
layout.left_drawer.add(item) # Ignored - already added
Group Rendering Options
Groups render differently depending on the zone:
In drawers (vertical layout):
- Groups stack vertically
- Dividers are horizontal lines
- Group labels appear above their content
In header/footer (horizontal layout):
- Groups arrange side-by-side
- Dividers are vertical lines
- Group labels are typically hidden
Advanced Features
Resizable Drawers
Both drawers can be resized by users via drag handles:
- Drag handle location:
- Left drawer: Right edge
- Right drawer: Left edge
- Width constraints: 150px (minimum) to 600px (maximum)
- Persistence: Resized width is automatically saved in the session state
Users can drag the handle to adjust drawer width. The new width is preserved throughout their session.
Programmatic Drawer Control
You can control drawers programmatically using commands:
# Toggle drawer visibility
toggle_left = layout.commands.toggle_drawer("left")
toggle_right = layout.commands.toggle_drawer("right")
# Update drawer width
update_left_width = layout.commands.update_drawer_width("left", width=300)
update_right_width = layout.commands.update_drawer_width("right", width=350)
These commands are typically used with buttons or other interactive elements:
# Add a button to toggle the right drawer
button = mk.button("Toggle Tools", command=layout.commands.toggle_drawer("right"))
layout.header_right.add(button)
State Persistence
The Layout automatically persists its state within the user's session:
| State Property | Description | Default |
|---|---|---|
left_drawer_open |
Whether left drawer is visible | True |
right_drawer_open |
Whether right drawer is visible | True |
left_drawer_width |
Left drawer width in pixels | 250 |
right_drawer_width |
Right drawer width in pixels | 250 |
State changes (toggle, resize) are automatically saved and restored within the session.
Dynamic Content Updates
Content zones can be updated dynamically during the session:
# Initial setup
layout.left_drawer.add(Div("Item 1"))
# Later, add more items (e.g., in a command handler)
def add_dynamic_content():
layout.left_drawer.add(Div("New Item"), group="dynamic")
return layout.left_drawer # Return updated drawer for HTMX swap
Note: When updating content dynamically, you typically return the updated zone to trigger an HTMX swap.
CSS Customization
The Layout uses CSS classes that you can customize:
| Class | Element |
|---|---|
mf-layout |
Root layout container |
mf-layout-header |
Header section |
mf-layout-footer |
Footer section |
mf-layout-main |
Main content area |
mf-layout-drawer |
Drawer container |
mf-layout-left-drawer |
Left drawer specifically |
mf-layout-right-drawer |
Right drawer specifically |
mf-layout-drawer-content |
Scrollable content within drawer |
mf-resizer |
Resize handle |
mf-layout-group |
Content group wrapper |
You can override these classes in your custom CSS to change colors, spacing, or behavior.
User Profile Integration
The Layout automatically includes a UserProfile component in the header right area. This component handles user authentication display and logout functionality when auth is enabled.
Examples
Example 1: Dashboard with Navigation Sidebar
A typical dashboard application with a navigation menu in the left drawer:
from fasthtml.common import *
from myfasthtml.controls.Layout import Layout
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
# Create layout
layout = Layout(parent=root_instance, app_name="Analytics Dashboard")
# Navigation menu in left drawer
def show_dashboard():
layout.set_main(Div(H1("Dashboard"), P("Overview of your metrics")))
return layout._mk_main()
def show_reports():
layout.set_main(Div(H1("Reports"), P("Detailed analytics reports")))
return layout._mk_main()
def show_settings():
layout.set_main(Div(H1("Settings"), P("Configure your preferences")))
return layout._mk_main()
# Add navigation items with groups
layout.left_drawer.add_group("main", group_ft=Div("MENU", cls="font-bold text-xs px-2 opacity-60"))
layout.left_drawer.add(mk.mk(Div("Dashboard"), command=Command("nav_dash", "Show dashboard", show_dashboard)),
group="main")
layout.left_drawer.add(mk.mk(Div("Reports"), command=Command("nav_reports", "Show reports", show_reports)),
group="main")
layout.left_drawer.add_group("config", group_ft=Div("CONFIGURATION", cls="font-bold text-xs px-2 opacity-60"))
layout.left_drawer.add(mk.mk(Div("Settings"), command=Command("nav_settings", "Show settings", show_settings)),
group="config")
# Header content
layout.header_left.add(Div("📊 Analytics", cls="font-bold"))
# Footer
layout.footer_left.add(Div("© 2024 Analytics Co."))
layout.footer_right.add(Div("v1.0.0"))
# Set initial main content
layout.set_main(Div(H1("Dashboard"), P("Overview of your metrics")))
Example 2: Development Tool with Debug Panel
An application with development tools in the right drawer:
from fasthtml.common import *
from myfasthtml.controls.Layout import Layout
from myfasthtml.controls.helpers import mk
# Create layout
layout = Layout(parent=root_instance, app_name="Dev Tools")
# Main content: code editor
layout.set_main(
Div(
H2("Code Editor"),
Textarea("# Write your code here", rows=20, cls="w-full font-mono")
)
)
# Right drawer: debug and tools
layout.right_drawer.add_group("debug", group_ft=Div("DEBUG INFO", cls="font-bold text-xs px-2 opacity-60"))
layout.right_drawer.add(Div("Console output here..."), group="debug")
layout.right_drawer.add(Div("Variables: x=10, y=20"), group="debug")
layout.right_drawer.add_group("tools", group_ft=Div("TOOLS", cls="font-bold text-xs px-2 opacity-60"))
layout.right_drawer.add(Button("Run Code"), group="tools")
layout.right_drawer.add(Button("Clear Console"), group="tools")
# Header
layout.header_left.add(Div("DevTools IDE"))
layout.header_right.add(Button("Save"))
Example 3: Minimal Layout (Main Content Only)
A simple layout without drawers, focusing only on main content:
from fasthtml.common import *
from myfasthtml.controls.Layout import Layout
# Create layout
layout = Layout(parent=root_instance, app_name="Simple Blog")
# Header
layout.header_left.add(Div("My Blog", cls="text-xl font-bold"))
layout.header_right.add(A("About", href="/about"))
# Main content
layout.set_main(
Article(
H1("Welcome to My Blog"),
P("This is a simple blog layout without side drawers."),
P("The focus is on the content in the center.")
)
)
# Footer
layout.footer_left.add(Div("© 2024 Blog Author"))
layout.footer_right.add(A("RSS", href="/rss"))
# Note: Drawers are present but can be collapsed by users if not needed
Example 4: Dynamic Content Loading
Loading content dynamically based on user interaction:
from fasthtml.common import *
from myfasthtml.controls.Layout import Layout
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
layout = Layout(parent=root_instance, app_name="Dynamic App")
# Function that loads content dynamically
def load_page(page_name):
# Simulate loading different content
content = {
"home": Div(H1("Home"), P("Welcome to the home page")),
"profile": Div(H1("Profile"), P("User profile information")),
"settings": Div(H1("Settings"), P("Application settings")),
}
layout.set_main(content.get(page_name, Div("Page not found")))
return layout._mk_main()
# Create navigation commands
pages = ["home", "profile", "settings"]
for page in pages:
cmd = Command(f"load_{page}", f"Load {page} page", load_page, page)
layout.left_drawer.add(
mk.mk(Div(page.capitalize()), command=cmd)
)
# Set initial content
layout.set_main(Div(H1("Home"), P("Welcome to the home page")))
Developer Reference
This section contains technical details for developers working on the Layout component itself.
State
The Layout component maintains the following state properties:
| Name | Type | Description | Default |
|---|---|---|---|
left_drawer_open |
boolean | True if the left drawer is open | True |
right_drawer_open |
boolean | True if the right drawer is open | True |
left_drawer_width |
integer | Width of the left drawer | 250 |
right_drawer_width |
integer | Width of the right drawer | 250 |
Commands
Available commands for programmatic control:
| Name | Description |
|---|---|
toggle_drawer(side) |
Toggles the drawer on the specified side |
update_drawer_width(side, width=None) |
Updates the drawer width on the specified side. The width is given by the HTMX request |
Public Methods
| Method | Description |
|---|---|
set_main(content) |
Sets the main content area |
render() |
Renders the complete layout |
High Level Hierarchical Structure
Div(id="layout")
├── Header
│ ├── Div(id="layout_hl")
│ │ ├── Icon # Left drawer icon button
│ │ └── Div # Left content for the header
│ └── Div(id="layout_hr")
│ ├── Div # Right content for the header
│ └── UserProfile # user profile icon button
├── Div # Left Drawer
├── Main # Main content
├── Div # Right Drawer
├── Footer # Footer
└── Script # To initialize the resizing
Element IDs
| Name | Description |
|---|---|
layout |
Root layout container (singleton) |
layout_h |
Header section (not currently used) |
layout_hl |
Header left side |
layout_hr |
Header right side |
layout_f |
Footer section (not currently used) |
layout_fl |
Footer left side |
layout_fr |
Footer right side |
layout_ld |
Left drawer |
layout_rd |
Right drawer |
Internal Methods
These methods are used internally for rendering:
| Method | Description |
|---|---|
_mk_header() |
Renders the header component |
_mk_footer() |
Renders the footer component |
_mk_main() |
Renders the main content area |
_mk_left_drawer() |
Renders the left drawer |
_mk_right_drawer() |
Renders the right drawer |
_mk_left_drawer_icon() |
Renders the left drawer toggle icon |
_mk_right_drawer_icon() |
Renders the right drawer toggle icon |
_mk_content_wrapper() |
Static method to wrap content with groups and dividers |
Content Class
The Layout.Content nested class manages content zones:
| Method | Description |
|---|---|
add(content, group=None) |
Adds content to a group, prevents duplicates based on ID |
add_group(group, group_ft=None) |
Creates a new group with optional custom header element |
get_content() |
Returns dictionary of groups and their content |
get_groups() |
Returns list of (group_name, group_ft) tuples |