Sheet

A Sheet (also known as a sidebar or slide-out panel) is a navigation component that slides in from the edges of the screen. It provides additional space for content without taking up the main view, and can be used for navigation menus, forms, or any supplementary content.

Install

#Import

use gpui_kit::component::WindowExt;
use gpui_kit::component::Placement;
// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { Sheet } from "sheet.slint";
use iced::widget::{column, container, mouse_area, row, scrollable, svg, text};
use iced::{Alignment, Border, Element, Length, Padding, Task, Theme};

#Usage

#Where sheets render

The window’s Root automatically mounts and renders sheets. Open the window with gpui_kit::open_window, or wrap the application view in Root::new. Application views do not render overlay layers themselves.

#Basic Sheet

window.open_sheet(cx, |sheet, _, _| {
    sheet
        .title("Navigation")
        .child("Sheet content goes here")
})
SlintNo Slint version of this example yet.
// The whole shape of the component: a title, a description, a body and a
// footer, closed by the header's control, the overlay or Escape.
let basic = column![
    Button::new("Open sheet")
        .outline()
        .on_press(Message::SetBasic(true)),
    Sheet::new(frame(
        320.0,
        "The panel covers this frame; the page around it is out of reach.",
    ))
    .open(app.basic_open)
    .title("Notifications")
    .description("You have three unread messages.")
    .body(
        column![
            text("The body scrolls if it needs to, and the header and footer stay put.")
                .size(theme::FONT_SIZE_SM)
        ]
        .spacing(theme::SPACING_SM),
    )
    .footer("Mark all as read")
    .on_footer_press(Message::SetBasic(false))
    .on_open_change(Message::SetBasic),
]
.spacing(theme::SPACING_MD);

#Sheet with Placement

// Left sheet (default)
window.open_sheet_at(Placement::Left, cx, |sheet, _, _| {
    sheet.title("Left Sheet")
})

// Right sheet
window.open_sheet_at(Placement::Right, cx, |sheet, _, _| {
    sheet.title("Right Sheet")
})

// Top sheet
window.open_sheet_at(Placement::Top, cx, |sheet, _, _| {
    sheet.title("Top Sheet")
})

// Bottom sheet
window.open_sheet_at(Placement::Bottom, cx, |sheet, _, _| {
    sheet.title("Bottom Sheet")
})
VerticalLayout {
    spacing: 12px;
    HorizontalLayout {
        spacing: 8px;
        alignment: start;
        Button {
            text: "Left sheet";
            outline: true;
            clicked => {
                root.where = Placement.left;
                root.open = true;
            }
        }
        Button {
            text: "Top sheet";
            outline: true;
            clicked => {
                root.where = Placement.top;
                root.open = true;
            }
        }
        Button {
            text: "Right sheet";
            outline: true;
            clicked => {
                root.where = Placement.right;
                root.open = true;
            }
        }
        Button {
            text: "Bottom sheet";
            outline: true;
            clicked => {
                root.where = Placement.bottom;
                root.open = true;
            }
        }
    }
    // The frame stands in for the window: the sheet fills it, and it is
    // clipped so the panel is hidden until it has slid in.
    Rectangle {
        width: 100%;
        height: 320px;
        border-width: 1px;
        border-color: Theme.border;
        border-radius: Theme.radius-lg;
        clip: true;
        Rectangle {
            width: 100%;
            height: 100%;
            background: Theme.muted.with-alpha(0.3);
        }
        Sheet {
            width: 100%;
            height: 100%;
            open <=> root.open;
            placement: root.where;
            title: root.edge-name(root.where);
            description: "Opened from the edge the button names.";
            Text {
                text: "Press a button above to slide the sheet in from another edge.";
                color: Theme.muted-foreground;
                font-size: Theme.font-size-xs;
                wrap: word-wrap;
            }
        }
    }
}
// One button per edge, sharing the frame: the panel is anchored to
// whichever side the button names.
let edges = row![
    Button::new("Left sheet")
        .outline()
        .on_press(Message::SetSide(Placement::Left)),
    Button::new("Top sheet")
        .outline()
        .on_press(Message::SetSide(Placement::Top)),
    Button::new("Right sheet")
        .outline()
        .on_press(Message::SetSide(Placement::Right)),
    Button::new("Bottom sheet")
        .outline()
        .on_press(Message::SetSide(Placement::Bottom)),
]
.spacing(theme::SPACING_SM);

let placement = column![
    edges,
    Sheet::new(frame(
        320.0,
        "The panel comes from the edge a button names."
    ))
    .open(app.placement_open)
    .placement(app.side)
    .title(edge_name(app.side))
    .description("Opened from the edge the button names.")
    .body(
        column![
            text("Press a button above to bring the sheet in from another edge.")
                .size(theme::FONT_SIZE_XS)
        ]
        .spacing(theme::SPACING_SM),
    )
    .on_open_change(Message::SetPlacement),
]
.spacing(theme::SPACING_MD);

#Sheet with Custom Size

window.open_sheet(cx, |sheet, _, _| {
    sheet
        .title("Wide Sheet")
        .size(px(500.))  // Custom width for left/right, height for top/bottom
        .child("This sheet is 500px wide")
})
SlintNo Slint version of this example yet.
// The panel's width for a left or right sheet — its height for a top or
// bottom one — is the edge's default until the caller names one.
let sized = column![
    Button::new("Open 420px sheet")
        .outline()
        .on_press(Message::SetSized(true)),
    Sheet::new(frame(
        320.0,
        "The panel is 420px wide: wider than this frame's default."
    ))
    .open(app.sized_open)
    .size(420.0)
    .title("Custom Size")
    .description("420px wide, where the default is 85% of the frame capped at 360.")
    .body(
        column![
            text(
                "Sheet::size is the Slint component's size: the width for a left or right \
                 sheet, the height for a top or bottom one. 0.0 keeps the edge's default."
            )
            .size(theme::FONT_SIZE_XS)
        ]
        .spacing(theme::SPACING_SM),
    )
    .on_open_change(Message::SetSized),
]
.spacing(theme::SPACING_MD);

#Sheet with Form Content

let input = cx.new(|cx| InputState::new(window, cx));
let date = cx.new(|cx| DatePickerState::new(window, cx));

window.open_sheet(cx, |sheet, _, _| {
    sheet
        .title("User Profile")
        .child(
            v_flex()
                .gap_4()
                .child("Enter your information:")
                .child(Input::new(&input).placeholder("Full Name"))
                .child(DatePicker::new(&date).placeholder("Date of Birth"))
        )
        .footer(
            h_flex()
                .gap_3()
                .child(Button::new("save").primary().label("Save"))
                .child(Button::new("cancel").label("Cancel"))
        )
})
SlintNo Slint version of this example yet.

#Overlay Options

window.open_sheet(cx, |sheet, _, _| {
    sheet
        .title("Settings")
        .overlay(true)              // Show overlay background (default: true)
        .overlay_closable(true)     // Click overlay to close (default: true)
        .child("Sheet settings content")
})

// No overlay
window.open_sheet(cx, |sheet, _, _| {
    sheet
        .title("Side Panel")
        .overlay(false)             // No overlay background
        .child("This sheet has no overlay")
})
SlintNo Slint version of this example yet.
// The scrim is always drawn; whether a click on it closes the panel is the
// switch iced has.
let overlay = column![
    Button::new("Open locked sheet")
        .outline()
        .on_press(Message::SetOverlay(true)),
    Sheet::new(frame(
        320.0,
        "The scrim is drawn; a click on it does not close this panel."
    ))
    .open(app.overlay_open)
    .title("Overlay Options")
    .description("The scrim blocks the page, and ignores a click on it.")
    .body(
        column![
            text(
                "Sheet::overlay_closable(false) leaves the header's × and Escape as the ways \
                 out. The iced sheet always draws the scrim; there is no overlay(false)."
            )
            .size(theme::FONT_SIZE_XS)
        ]
        .spacing(theme::SPACING_SM),
    )
    .overlay_closable(false)
    .on_open_change(Message::SetOverlay),
]
.spacing(theme::SPACING_MD);

#Resizable Sheet

window.open_sheet(cx, |sheet, _, _| {
    sheet
        .title("Resizable Panel")
        .resizable(true)            // Allow user to resize (default: true)
        .size(px(300.))
        .child("You can resize this sheet by dragging the edge")
})
SlintNo Slint version of this example yet.

#Custom Margin and Positioning

window.open_sheet(cx, |sheet, _, _| {
    sheet
        .title("Below Title Bar")
        .margin_top(px(32.))        // Space for window title bar
        .child("This sheet appears below the title bar")
})
SlintNo Slint version of this example yet.

#Sheet with List

let delegate = ListDelegate::new(items);
let list = cx.new(|cx| List::new(delegate, window, cx));

window.open_sheet_at(Placement::Left, cx, |sheet, _, _| {
    sheet
        .title("File Explorer")
        .size(px(400.))
        .child(
            div()
                .border_1()
                .border_color(cx.theme().border)
                .rounded(cx.theme().radius)
                .size_full()
                .child(list.clone())
        )
})
Rectangle {
    width: 100%;
    height: 360px;
    border-width: 1px;
    border-color: Theme.border;
    border-radius: Theme.radius-lg;
    clip: true;
    Rectangle {
        width: 100%;
        height: 100%;
        background: Theme.muted.with-alpha(0.3);
    }
    Sheet {
        width: 100%;
        height: 100%;
        open: true;
        size: 280px;
        title: "File Explorer";
        description: "40 files. The list scrolls; the header does not.";
        footer-label: "Download all";
        for i in 40: Rectangle {
            height: 30px;
            border-radius: Theme.radius-sm;
            background: Theme.muted.with-alpha(0.4);
            HorizontalLayout {
                padding-left: 10px;
                padding-right: 10px;
                cross-axis-alignment: center;
                Text {
                    text: "report-\{i + 1}.pdf";
                    color: Theme.foreground;
                    font-size: Theme.font-size-xs;
                    vertical-alignment: center;
                }
            }
        }
    }
}
// The body scrolls inside the panel, so the header and the footer stay put
// however long the list is.
let list = Sheet::new(frame(
    360.0,
    "40 files. The list scrolls; the header does not.",
))
.open(app.list_open)
.size(280.0)
.title("File Explorer")
.description("40 files. The list scrolls; the header does not.")
.body(column((0..40).map(file_row)).spacing(theme::SPACING_SM))
.footer("Download all")
.on_footer_press(Message::SetList(false))
.on_open_change(Message::SetList);

#Close Event Handling

window.open_sheet(cx, |sheet, _, _| {
    sheet
        .title("Sheet with Handler")
        .child("This sheet has a custom close handler")
        .on_close(|_, window, cx| {
            window.push_notification("Sheet was closed", cx);
        })
})
SlintNo Slint version of this example yet.
window.open_sheet_at(Placement::Left, cx, |sheet, _, _| {
    sheet
        .title("Navigation")
        .size(px(280.))
        .child(
            v_flex()
                .gap_2()
                .child(Button::new("home").ghost().label("Home").w_full())
                .child(Button::new("profile").ghost().label("Profile").w_full())
                .child(Button::new("settings").ghost().label("Settings").w_full())
                .child(Button::new("logout").ghost().label("Logout").w_full())
        )
})
SlintNo Slint version of this example yet.

#Custom Styling

window.open_sheet(cx, |sheet, _, cx| {
    sheet
        .title("Styled Sheet")
        .bg(cx.theme().accent)
        .text_color(cx.theme().accent_foreground)
        .border_color(cx.theme().primary)
        .child("Custom styled sheet content")
})
SlintNo Slint version of this example yet.

#Programmatic Close

// Close sheet from inside
Button::new("close")
    .label("Close Sheet")
    .on_click(|_, window, cx| {
        window.close_sheet(cx);
    })

// Close sheet from outside
window.close_sheet(cx);
SlintNo Slint version of this example yet.

#Settings Panel

window.open_sheet_at(Placement::Right, cx, |sheet, _, _| {
    sheet
        .title("Settings")
        .size(px(350.))
        .child(
            v_flex()
                .gap_4()
                .child("Appearance")
                .child(Checkbox::new("dark-mode").label("Dark Mode"))
                .child(Checkbox::new("animations").label("Enable Animations"))
                .child("Notifications")
                .child(Checkbox::new("push-notifications").label("Push Notifications"))
        )
        .footer(
            h_flex()
                .justify_end()
                .gap_2()
                .child(Button::new("apply").primary().label("Apply"))
                .child(Button::new("cancel").label("Cancel"))
        )
})
SlintNo Slint version of this example yet.

#File Browser

window.open_sheet_at(Placement::Left, cx, |sheet, _, _| {
    sheet
        .title("Files")
        .size(px(300.))
        .child(
            v_flex()
                .size_full()
                .child(
                    h_flex()
                        .gap_2()
                        .p_2()
                        .child(Button::new("new-folder").small().icon(IconName::FolderPlus))
                        .child(Button::new("upload").small().icon(IconName::Upload))
                )
                .child(
                    div()
                        .flex_1()
                        .overflow_hidden()
                        .child(file_tree_list)
                )
        )
})
SlintNo Slint version of this example yet.

#Help Panel

window.open_sheet_at(Placement::Bottom, cx, |sheet, _, _| {
    sheet
        .title("Help & Documentation")
        .size(px(200.))
        .child(
            h_flex()
                .gap_4()
                .child("Keyboard Shortcuts")
                .child(Kbd::new("⌘").child("K"))
                .child("Search")
                .child(Kbd::new("⌘").child("P"))
                .child("Command Palette")
        )
})
SlintNo Slint version of this example yet.

#Best Practices

  1. Appropriate Placement: Use left/right for navigation, top/bottom for temporary content
  2. Consistent Sizing: Maintain consistent sheet sizes across your application
  3. Clear Headers: Always provide descriptive titles
  4. Close Options: Provide multiple ways to close (ESC, overlay click, close button)
  5. Content Organization: Use proper spacing and grouping for sheet content
  6. Responsive Design: Consider sheet behavior on smaller screens
  7. Performance: Lazy load sheet content when possible for better performance

#API Reference

#Window Extensions

MethodDescription
open_sheet(cx, fn)Open sheet with default placement (Right)
open_sheet_at(placement, cx, fn)Open sheet at specific placement
close_sheet(cx)Close current sheet

#Sheet Builder

MethodDescription
title(str)Set sheet title
child(el)Add content to sheet body
footer(el)Set footer content
size(px)Set sheet size (width or height)
margin_top(px)Set top margin (for title bars)
resizable(bool)Allow resizing (default: true)
overlay(bool)Show overlay background (default: true)
overlay_closable(bool)Click overlay to close (default: true)
on_close(fn)Close event callback

#Placement Options

ValueDescription
Placement::LeftSlides in from left edge
Placement::RightSlides in from right edge (default)
Placement::TopSlides in from top edge
Placement::BottomSlides in from bottom edge

#Styling Methods

MethodDescription
bg(color)Set background color
text_color(color)Set text color
border_color(color)Set border color
px_*()/py_*()Custom padding
gap_*()Spacing between children

#Sheet

A panel that slides in from an edge of its container, following shadcn's `Sheet`: an overlay, a header of title and description with a close control, a body that scrolls, and an optional footer action. The panel travels off its own edge rather than being removed, so it is in the tree for the whole of both transitions. Give it a container the size of the window it stands in for.

NameKindTypeDefaultDescription
openin-outbool
titleinstring
descriptioninstring
placementinPlacementPlacement.rightThe edge the panel comes from.
sizeinlength0pxThe panel's width for a left or right sheet, its height for a top or bottom one. Zero keeps the default for that edge.
overlay-closableinbooltrueWhether a click on the overlay closes the sheet.
closedcallback()