Files
MyFastHtml/docs/Layout.md

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