Drawer
A static page with IzzyBlazor's script. No circuit.
Usage
<DialogTrigger For="filters">Filters</DialogTrigger>
<Drawer Id="filters" Title="Filters" Size="DialogSize.Sm" @bind-Open="_open">
<Form Model="Filters" FormName="filters" OnValidSubmit="ApplyAsync">
<DialogBody Gap="5">
<RadioGroup @bind-Value="Filters!.Status" />
</DialogBody>
<FormActions>
<Button Submit>Apply</Button>
</FormActions>
</Form>
</Drawer>
@* ApplyAsync ends with _open = false, which closes it on an interactive page. *@Opening it
A drawer is a Dialog, so it opens the same three ways: a DialogTrigger with its Id, a link that adds ?dialog= and the id to the address, or @bind-Open from C#. The trigger and the link work on static pages, with JavaScript off too; Dialog's Overview shows each in every mode.
A form in a drawer
Put a Form in the drawer with its fields in a DialogBody and its FormActions last: the body fills the drawer's height, so the buttons sit at its bottom as the footer and still submit the form.
Preview
Code
System.ComponentModel.DataAnnotations
<Row Gap="3" Wrap Align="Align.Center">
<DialogTrigger For="order-filters" StartIcon="Lucide.SlidersHorizontal" Variant="ButtonVariant.Outline" Accent="Accent.Neutral">Filters</DialogTrigger>
<TextBlock As="span" Size="TextSize.Sm" Color="Color.Muted">_summary</TextBlock>
</Row>
<Drawer Id="order-filters" Title="Filters" Size="DialogSize.Sm" @bind-Open="_open">
<Form Model="Model" FormName="order-filters" OnValidSubmit="Apply">
@* DialogBody fills the drawer's height, so the footer after it sits at the bottom. *@
<DialogBody Gap="5">
<RadioGroup @bind-Value="Model!.Status" />
<Checkbox @bind-Value="Model!.PaidOnly" />
<Checkbox @bind-Value="Model!.Archived" />
</DialogBody>
@* Last in the form, the button is the drawer's footer and still submits it. Choices stay in
the drawer until it's pressed; the close button leaves the shown orders as they are. *@
<FormActions>
<Button Submit>Show orders</Button>
</FormActions>
</Form>
</Drawer>
{
[SupplyParameterFromForm(FormName = "order-filters")]
private OrderFilters? Model { get; set; }
private bool _open;
private string _summary = "Showing all orders";
protected override void OnInitialized() => Model ??= new();
private void Apply()
{
var status = Model!.Status == OrderStatus.Any ? "all" : Model.Status.ToString().ToLowerInvariant();
_summary = $"Showing {status} orders" + (Model.PaidOnly ? ", paid only" : "") + (Model.Archived ? ", archived too" : "");
// On an interactive page C# closes the drawer; a post on a static page closes it anyway.
_open = false;
}
public sealed class OrderFilters
{
public OrderStatus Status { get; set; }
[Display(Name = "Paid orders only")]
public bool PaidOnly { get; set; }
[Display(Name = "Include archived orders")]
public bool Archived { get; set; }
}
public enum OrderStatus { [Display(Name = "Any status")] Any, Pending, Shipped, Delivered }
}Static: Show orders posts without a reload, and the drawer closes.
A post on a static page closes the drawer. On an interactive page the submit runs C# instead, so close it there with @bind-Open, as this example does. When a post can fail, reopen it with the errors as Dialog's form example does.
Edges and widths
Use Size for the width (Sm 20rem, Md 24rem by default, Lg 32rem, Xl 40rem, or Full) or Width for another, and Placement for another edge: Start, or Bottom and Top for a full-width sheet as tall as its content.
Preview
Code
<Row Gap="3" Wrap Justify="Justify.Center">
<DialogTrigger For="order-details" Variant="ButtonVariant.Outline" Accent="Accent.Neutral" StartIcon="Lucide.PanelRight">End</DialogTrigger>
<DialogTrigger For="order-history" Variant="ButtonVariant.Outline" Accent="Accent.Neutral" StartIcon="Lucide.PanelRight">End, large</DialogTrigger>
<DialogTrigger For="order-share" Variant="ButtonVariant.Outline" Accent="Accent.Neutral" StartIcon="Lucide.PanelBottom">Bottom</DialogTrigger>
</Row>
@* The default: the end edge, Size Md (24rem). *@
<Drawer Id="order-details" Title="Order #1042">
<TextBlock>Two items, paid by card, shipping to London.</TextBlock>
</Drawer>
@* Lg, 32rem: room for a table or a longer form. On phones every size stops 3rem short of the
screen's width. *@
<Drawer Id="order-history" Title="Order history" Size="DialogSize.Lg">
<DescriptionList>
<DescriptionItem Term="Oct 20">Placed, two items</DescriptionItem>
<DescriptionItem Term="Oct 21">Paid by card ending 4242</DescriptionItem>
<DescriptionItem Term="Oct 22">Shipped from Leeds</DescriptionItem>
</DescriptionList>
</Drawer>
@* A sheet along the bottom, as tall as its content: suits phones. *@
<Drawer Id="order-share" Title="Share this order" Placement="DialogPlacement.Bottom">
<TextBlock>Anyone with the link can see the order's items and status.</TextBlock>
</Drawer>A menu from the start edge
Use Placement="DialogPlacement.Start" with Size="DialogSize.Sm" for navigation that slides in, such as a site's menu on phones.
An app's navigation doesn't need one: in an AppShell, the AppSidebar becomes a drawer below its breakpoint by itself, opened by a SidebarToggle, with JavaScript off too.
Header, footer and closing
Use a DialogBody for the content and a DialogFooter after it: the body fills the drawer's height, so the footer sits at its bottom. Without a body, the footer follows the content partway down.
Everything else is the dialog's: Title or a DialogHeader, DialogBody, DialogFooter and DialogClose, CloseOnBackdrop and ShowCloseButton, and InsideForm. Dialog's Overview covers each.
Mode support
| Feature | Off | Static | Live |
|---|---|---|---|
| Everything | Partly | Yes | Yes |
Off: JavaScript disabled. Static: a static SSR page with IzzyBlazor's script. Live: an interactive page (Server, WebAssembly or Auto). With JavaScript off, a drawer opened from the server isn't modal: focus can leave it, and Escape or a click outside closes it only in browsers with closedby (not Safari).
Accessibility
- It's a native modal
dialog: the page behind it is inert, Escape closes it, and focus returns to theDialogTriggerthat opened it. Titleor aDialogHeadernames it; without either, pass anaria-label.- The close button is named "Close" (
CloseLabel). - With JavaScript off, a drawer opened from the server isn't modal: focus can leave it for the page behind. In browsers with the
closedbyattribute (Chrome and Edge 134, Firefox 141; not Safari), Escape closes it, and so does a click outside whileCloseOnBackdropis on.
Parameters
| Prop | Type | Default |
|---|---|---|
TitleThe heading, which also names the dialog for screen readers. | string? | |
CloseLabelThe close button's name. | string? | "Close" |
ChildContentThe body, plus any DialogHeader and DialogFooter. | RenderFragment? | |
ContentPaddingPadding inside the body, in spacing steps, in place of the dialog's. | Responsive<int> | |
SizeThe width. | DialogSize | DialogSize.Md |
PlacementCenter (the default), a full-height drawer on the start or end side, or a full-width sheet along the top or bottom. | DialogPlacement | DialogPlacement.End |
CloseOnBackdropCloses when the backdrop is clicked. | bool | true |
ShowCloseButtonShows the close button in the header. | bool | true |
InsideFormSet when the dialog is inside a <form>. | bool | false |
QueryKeyThe query-string key that opens a dialog from a link: ?dialog=my-id renders the dialog with that id already open. | string? | "dialog" |
@bind-OpenWhether it is open. | bool | false |
IdThe element id. | string? |
Plus 16 item props and 4 common props.
Full APISee also
- Dialog: a native modal dialog, opened by a trigger, a link or C#.
- AppSidebar: a sidebar that becomes a drawer or panel on small screens.
- Popover: non-modal content beside a trigger.