Dialog
An accessible modal with focus trap, scroll lock, and inert background.
Basic Dialog
Click the button to open the dialog. While open, focus is trapped inside the
panel, body scroll is locked, the rest of the page is marked inert,
and Escape or clicking the backdrop closes it. Focus is returned to the
opener button on close.
<BitBhButton OnClick="() => isOpen = true" class="btn btn-primary">
Open dialog
</BitBhButton>
<BitBhDialog Open="isOpen" OnClose="() => isOpen = false">
<BitBhDialogBackdrop class="dialog-backdrop" />
<BitBhDialogPanel class="dialog-panel">
<BitBhDialogTitle class="dialog-title">Welcome aboard</BitBhDialogTitle>
<BitBhDialogDescription class="dialog-description">
This is a fully-accessible dialog. Try Tab and Shift+Tab — focus
cycles inside this panel. Press Escape to close.
</BitBhDialogDescription>
<div class="dialog-actions">
<BitBhButton OnClick="() => isOpen = false" class="btn">Cancel</BitBhButton>
<BitBhButton OnClick="() => isOpen = false" class="btn btn-primary">Got it</BitBhButton>
</div>
</BitBhDialogPanel>
</BitBhDialog>
@code {
private bool isOpen;
}/* The library applies minimal default positioning (position:fixed inset:0)
so consumers can layer the backdrop and panel inside it. Everything else is yours. */
.dialog-backdrop {
background: rgba(0, 0, 0, 0.5);
}
.dialog-panel {
/* Centre the panel in the viewport */
margin: 4rem auto;
max-width: 28rem;
background: #fff;
border-radius: 0.75rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.85rem;
}
.dialog-title { font-size: 1.1rem; font-weight: 600; color: #18181b; }
.dialog-description { font-size: 0.9rem; color: #71717a; }
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.5rem;
}Confirmation (no backdrop dismiss)
Set DismissOnClick="false" on the backdrop and
DismissOnEscape="false" on the panel to require an explicit
choice. Useful for destructive actions.
<BitBhButton OnClick="() => isOpen = true" class="btn">
Delete account
</BitBhButton>
<span class="demo-output">@confirmResult</span>
<BitBhDialog Open="isOpen" OnClose="() => isOpen = false">
<BitBhDialogBackdrop class="dialog-backdrop" DismissOnClick="false" />
<BitBhDialogPanel class="dialog-panel" DismissOnEscape="false">
<BitBhDialogTitle class="dialog-title">Delete account?</BitBhDialogTitle>
<BitBhDialogDescription class="dialog-description">
This will permanently remove your account and all its data.
This cannot be undone.
</BitBhDialogDescription>
<div class="dialog-actions">
<BitBhButton OnClick="HandleCancel" class="btn">Cancel</BitBhButton>
<BitBhButton OnClick="HandleDelete" class="btn btn-danger">Yes, delete</BitBhButton>
</div>
</BitBhDialogPanel>
</BitBhDialog>
@code {
private bool isOpen;
private string? confirmResult;
private void HandleCancel()
{
isOpen = false;
confirmResult = "Cancelled.";
}
private void HandleDelete()
{
isOpen = false;
confirmResult = "Account deleted (just kidding).";
}
}/* The library applies minimal default positioning (position:fixed inset:0)
so consumers can layer the backdrop and panel inside it. Everything else is yours. */
.dialog-backdrop {
background: rgba(0, 0, 0, 0.5);
}
.dialog-panel {
/* Centre the panel in the viewport */
margin: 4rem auto;
max-width: 28rem;
background: #fff;
border-radius: 0.75rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.85rem;
}
.dialog-title { font-size: 1.1rem; font-weight: 600; color: #18181b; }
.dialog-description { font-size: 0.9rem; color: #71717a; }
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.5rem;
}
.btn-danger {
background: #dc2626;
border-color: #dc2626;
color: #fff;
}
.btn-danger:hover {
background: #b91c1c;
border-color: #b91c1c;
}With form fields
The dialog also works as a focusable form container. Tab cycles between the inputs and buttons; the focus trap keeps them all reachable while keeping the background untouchable.
<BitBhButton OnClick="() => isOpen = true" class="btn btn-primary">
Edit profile
</BitBhButton>
<span class="demo-output">@formResult</span>
<BitBhDialog Open="isOpen" OnClose="() => isOpen = false">
<BitBhDialogBackdrop class="dialog-backdrop" />
<BitBhDialogPanel class="dialog-panel">
<BitBhDialogTitle class="dialog-title">Edit profile</BitBhDialogTitle>
<BitBhDialogDescription class="dialog-description">
Update your display name. Press Escape or click outside to cancel.
</BitBhDialogDescription>
<label class="dialog-field">
<span>Display name</span>
<input type="text" @bind="displayName" class="dialog-input" />
</label>
<div class="dialog-actions">
<BitBhButton OnClick="() => isOpen = false" class="btn">Cancel</BitBhButton>
<BitBhButton OnClick="HandleSave" class="btn btn-primary">Save</BitBhButton>
</div>
</BitBhDialogPanel>
</BitBhDialog>
@code {
private bool isOpen;
private string? formResult;
private string displayName = "Alice";
private void HandleSave()
{
isOpen = false;
formResult = $"Saved: {displayName}";
}
}/* The library applies minimal default positioning (position:fixed inset:0)
so consumers can layer the backdrop and panel inside it. Everything else is yours. */
.dialog-backdrop {
background: rgba(0, 0, 0, 0.5);
}
.dialog-panel {
/* Centre the panel in the viewport */
margin: 4rem auto;
max-width: 28rem;
background: #fff;
border-radius: 0.75rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.85rem;
}
.dialog-title { font-size: 1.1rem; font-weight: 600; color: #18181b; }
.dialog-description { font-size: 0.9rem; color: #71717a; }
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.5rem;
}
.dialog-field {
display: flex;
flex-direction: column;
gap: 0.35rem;
font-size: 0.85rem;
color: #18181b;
}
.dialog-input {
padding: 0.45rem 0.6rem;
border: 1px solid #d4d4d8;
border-radius: 0.4rem;
font-size: 0.9rem;
font-family: inherit;
}
.dialog-input:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 1px;
}