BottomSheet
BottomSheet apresenta uma tarefa focada a partir da borda inferior da viewport. Escolha-o para uma tarefa secundária pensada primeiro para mobile; escolha Drawer para um painel lateral adequado a um fluxo desktop.
Exemplos
Detalhes do projeto
Forneça um title visível tanto para o cabeçalho quanto para o nome acessível do painel. Passe
closeLabel sempre que o nome padrão em inglês do botão de fechar não for adequado ao produto.
Um painel de ações sem título visível
Quando o painel não tiver title visível, aria-label é obrigatório para nomear o diálogo modal.
O closeLabel localizado continua nomeando o botão de fechar.
Quando usar
Use BottomSheet para uma tarefa curta, pensada primeiro para mobile, ou detalhes que devem surgir da parte inferior da visualização atual.
Prefira outro componente quando:
- A tarefa pertence a um painel lateral, especialmente no desktop — use Drawer.
- Uma decisão bloqueante precisa de um modal central e explícito — use Dialog.
- O conteúdo é ancorado a um controle pequeno e não deve bloquear a página — use Popover.
Acessibilidade
- BottomSheet é um
role="dialog"portado, comaria-modal="true". Umtitlevisível o nomeia comaria-labelledby; sem título, umaria-labeltraduzido é obrigatório. - On open, focus moves to a safe initial destination in the sheet and stays trapped there. Background
scroll is locked. Declare
initialFocusTowhen the application owns the task meaning, andreturnFocusTofor a pointer-opened workflow that needs an explicit return destination. - Escape, o botão de fechar e um clique genuíno no backdrop chamam
onClose. O backdrop registra onde o mousedown começou, então arrastar do painel para o backdrop não pode fechá-lo. - O painel usa portal para
document.body, a menos quecontainerforneça outro host de portal. Ele continua montada durante a saída animada; preferências de movimento reduzido desativam a animação. - O botão de fechar usa por padrão o nome acessível em inglês "Close". Passe
closeLabelpara traduzi-lo.
React initial focus
initialFocusTo is optional and has the type () => HTMLElement | null. For each accepted opening
of a visible modal panel, Lyra synchronously calls the read-only resolver once. Return a current,
eligible member of that panel; applications choose a least-destructive Cancel action for a
confirmation, a named tabIndex={-1} reading heading for content, or the intended field for a form.
An eligible target is an HTMLElement in the panel's document and current modal panel, connected,
visible with rendered rectangles, enabled (including its fieldset), programmatically focusable, and
outside hidden, inert, or aria-hidden ancestry. A null or invalid result focuses the named panel
directly; it never falls through to another control. Omitting the option uses the first eligible task
control, then the panel. The resolver is not replayed on rerenders; a later accepted opening reads
the current resolver.
Form composition owns focus after failed validation; initial entry does not infer failure from
aria-invalid. The resolver only reads committed state and refs: do not focus, mutate, or start
asynchronous work. If it throws, Lyra focuses the panel and propagates the same error through React's
effect handling.
React return focus
returnFocusTo is optional and has the type () => HTMLElement | null. It resolves freshly after
an accepted close, so it can read current workflow state and refs instead of preserving a stale
element. Use it for a stable, eligible trigger; when that trigger can be removed, disabled, hidden,
or no longer meaningful, return a logical successor instead.
An eligible destination is connected in the same document, visible, enabled, programmatically
focusable, and outside the closing sheet and overlay. A composition must not return a target that it
removes while closing. Omitting returnFocusTo retains only a valid captured opener that was already
focused, such as after keyboard opening. Unprepared WebKit mouse usage without a usable explicit
target remains unqualified: adding this optional prop does not repair unmigrated consumers.
import { BottomSheet, Button } from '@lyra-ds/react';
import { useRef, useState } from 'react';
function ProjectDetails({ canReturnToTrigger }: { canReturnToTrigger: boolean }) {
const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const successorRef = useRef<HTMLHeadingElement>(null);
return (
<>
<Button ref={triggerRef} disabled={!canReturnToTrigger} onClick={() => setOpen(true)}>
Open project details
</Button>
<h2 ref={successorRef} tabIndex={-1}>
Project overview
</h2>
<BottomSheet
open={open}
onClose={() => setOpen(false)}
returnFocusTo={() =>
canReturnToTrigger ? (triggerRef.current ?? successorRef.current) : successorRef.current
}
title="Project details"
>
Review the latest milestone before you continue.
</BottomSheet>
</>
);
}Project overview is a named, meaningful tabIndex={-1} destination. The
canReturnToTrigger state represents current availability; a non-null ref alone does not make a
target eligible. A removed trigger also falls through its null ref to the successor.
After an accepted close, Lyra prefers an eligible resolver result, then only a still-eligible
captured opener. It ignores an ineligible resolver result and never picks an arbitrary page control
or body as a fallback. If neither is eligible, Lyra makes no invalid focus call and the controlled
close still proceeds; the browser may land on body. This is an invalid composition, and the
application must provide a logical successor. One development diagnostic reports the invalid
composition for that close cycle; it is not successful focus restoration. The resolver must only
read current state and refs: do not focus, mutate the DOM, or start asynchronous work in it.
An ignored onClose request leaves the sheet open: Lyra does not resolve returnFocusTo or move
focus. Once the sheet is closed, rerenders while open remains false do not restore focus again.
API e código
| Nome | Tipo | Obrigatória | Descrição |
|---|---|---|---|
open | boolean | Obrigatória | Controls visibility. `true` mounts the portaled overlay and bottom sheet. |
onClose | () => void | — | Called when the user dismisses the sheet with Escape, the backdrop, or the close button. |
closeLabel | string | — | Accessible name for the close button. Default: `"Close"`. |
container | HTMLElement | — | Portal host. Defaults to `document.body`. |
returnFocusTo | () => HTMLElement | null | — | Resolves the logical destination for an accepted close. The resolver result is used only when eligible; otherwise an eligible previously focused opener is the fallback. A successor composition must supply a meaningful target when the opener can disappear or become ineligible. |
initialFocusTo | () => HTMLElement | null | — | Resolves the initial focus destination inside the modal on each accepted opening. |
children | ReactNode | Obrigatória | Bottom sheet body content. |
title | Exclude<ReactNode, boolean | null | undefined> | undefined | — | Heading rendered in the sheet header and used as its accessible name. Omit the heading when the sheet has no visible title. |
'aria-label' | never | string | — | The visible heading supplies the accessible name in this branch. Translated accessible name required when no heading is rendered. |
x-data="lyraBottomSheet({ … })"
| Opção | Tipo | Obrigatória | Descrição |
|---|---|---|---|
defaultOpen | boolean | — | Whether the sheet starts open. Defaults to `false`. |
returnFocusTo | () => HTMLElement | null | — | Resolves the current logical focus destination after an accepted modal close. |
After the existing Alpine plugin installation (Alpine.plugin(lyra)), use the registered
lyraBottomSheet binding; do not import its internal factory. The plugin owns opening and closing,
focus trapping, scroll locking, Escape, and the guarded backdrop click. It does not portal this
consumer-served composition automatically.
returnFocusTo is initial binding configuration, not mutable callback data. On an accepted close
it synchronously reads current state or refs once: Lyra prefers its eligible explicit target with
focus({ preventScroll: true }), then an eligible captured opener with ordinary focus() and
scroll behavior.
The target must be connected, visible, enabled, focusable, in the same document, outside the closing
overlay and panel, and outside hidden, inert, or aria-hidden content. There is no arbitrary
body fallback. Return a meaningful successor when the trigger is removed or becomes unsafe; do
not focus, mutate, or start asynchronous work from the resolver. This example treats an absent,
disabled, or hidden trigger as unavailable and returns the named successor. Other application
availability changes, including hidden or inert ancestry and a no-longer-meaningful trigger, must
choose their meaningful successor from current state; this example covers its named simple states,
not arbitrary automatic discovery. Initial closed state, opening, ignored close requests, exit, and
destroy do not resolve it.
<div
x-data="lyraBottomSheet({
returnFocusTo: () => {
const trigger = document.getElementById('open-project-sheet');
return trigger instanceof HTMLButtonElement && !trigger.disabled && !trigger.hidden
? trigger
: document.getElementById('project-overview');
},
})"
>
<button
id="open-project-sheet"
class="lyra-btn lyra-btn--primary lyra-btn--md"
type="button"
@click="open = true"
>
Open project details
</button>
<h2 id="project-overview" tabindex="-1">Project overview</h2>
<div class="lyra-bottomsheet-overlay" x-bind="overlay" style="display: none">
<div
class="lyra-bottomsheet"
role="dialog"
aria-modal="true"
aria-labelledby="sheet-title"
tabindex="-1"
x-bind="panel"
>
<div class="lyra-bottomsheet__header">
<h2 id="sheet-title" class="lyra-bottomsheet__title">Project details</h2>
<button class="lyra-bottomsheet__close" aria-label="Close" x-bind="close">×</button>
</div>
<div class="lyra-bottomsheet__body">
Review the latest milestone, owners and activity before you continue.
</div>
</div>
</div>
</div><lyra:bottom-sheet> Gerado do lyra-ds/blade v0.10.0.
O comportamento vem de lyraBottomSheet() — instale @lyra-ds/alpine e veja a aba HTML + Alpine.
| Prop | Padrão | Obrigatória | Valores de exemplo |
|---|---|---|---|
title | null | — | — |
ariaLabel | null | — | — |
closable | true | — | — |
closeLabel | 'Close' | — | — |
defaultOpen | false | — | — |
<lyra:bottom-sheet title="Share project" close-label="Close">
Anyone with the link can view this project.
</lyra:bottom-sheet>