Drawer
Drawer é um painel lateral controlado que entra pela direita. Escolha-o para um trabalho que fica ao lado da página; escolha Dialog quando a decisão precisar bloquear a página até ser resolvida.
Exemplos
Detalhes com ação fixa
Coloque no footer a ação que conclui a tarefa lateral. O painel mantém a página atrás dele,
então a pessoa preserva seu contexto enquanto revisa ou altera algo pequeno.
Detalhes somente leitura
Omita footer quando não houver ação para fixar. Isso remove inteiramente o chrome do rodapé, em
vez de deixar um divisor vazio no fim do painel.
Quando usar
Use Drawer para detalhes ou uma tarefa curta que se beneficia de manter a página visível: inspecionar um registro de atividade, editar poucos campos ou revisar contexto antes de uma ação.
Prefira outro componente quando:
- Uma decisão precisa interromper o progresso — use Dialog, um modal central que deixa explícita a escolha bloqueante.
- A tarefa precisa de destino próprio, histórico ou URL compartilhável — use uma rota. Um drawer mantém a página atual atrás dele; uma página inteira não.
- A mensagem só confirma algo que já aconteceu — use Toast ou Alert inline.
Acessibilidade
- O painel é
role="dialog"comaria-modal="true", nomeado pelotitleviaaria-labelledby.titleé o cabeçalho, não o atributo de tooltip HTML nativo. - On open, focus moves to a safe initial destination in the panel and stays trapped there. For React,
declare
initialFocusTowhen the application owns the task meaning, andreturnFocusTowhen a pointer-opened workflow needs an explicit return destination; body scroll is locked while it is open. - Escape, o botão de fechar e um clique no backdrop chamam
onClose. O backdrop é uma conveniência apenas para ponteiro; quem usa teclado fecha com Escape ou com o botão de fechar. - O nome acessível do botão de fechar tem como padrão a string em inglês "Close". Passe
closeLabelpara traduzi-la em produtos localizados. - Por padrão, o componente é portado para
document.body. Passecontainerapenas quando a aplicação precisar de outro host de portal.
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 own whether this is a least-destructive Cancel action, a
named reading heading, or an intended form field. The live DrawerWithoutFooter example enters its
read-only activity detail at the named tabIndex={-1} heading and intentionally has no footer.
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 the 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 panel 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.
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>
<Drawer
open={open}
onClose={() => setOpen(false)}
returnFocusTo={() =>
canReturnToTrigger ? (triggerRef.current ?? successorRef.current) : successorRef.current
}
title="Project details"
>
Review the latest deployment before you continue.
</Drawer>
</>
);
}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 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.
API e código
| Nome | Tipo | Obrigatória | Descrição |
|---|---|---|---|
open | boolean | Obrigatória | Controls visibility. `true` mounts the portaled overlay and drawer panel. |
onClose | () => void | — | Called when the user dismisses the drawer with Escape, the backdrop, or the close button. |
closeLabel | string | — | Accessible name for the close button. Default: `"Close"`. |
title | ReactNode | Obrigatória | Heading rendered in the drawer header and used as its accessible name. |
footer | ReactNode | — | Fixed actions rendered in the footer. Omit to remove the footer chrome. |
container | HTMLElement | — | Portal host. Defaults to `document.body`. |
returnFocusTo | () => HTMLElement | null | — | Resolves the current logical destination for focus after an accepted close. |
initialFocusTo | () => HTMLElement | null | — | Resolves the initial focus destination inside the modal on each accepted opening. |
children | ReactNode | Obrigatória | Drawer body content. |
x-data="lyraDrawer({ … })"
| Opção | Tipo | Obrigatória | Descrição |
|---|---|---|---|
defaultOpen | boolean | — | |
labelId | string | — | |
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
lyraDrawer 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="lyraDrawer({
returnFocusTo: () => {
const trigger = document.getElementById('open-project-details');
return trigger instanceof HTMLButtonElement && !trigger.disabled && !trigger.hidden
? trigger
: document.getElementById('project-overview');
},
})"
>
<button
id="open-project-details"
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-drawer-overlay" x-bind="overlay" style="display: none">
<div class="lyra-drawer" x-bind="panel">
<div class="lyra-drawer__header">
<h2 class="lyra-drawer__title" x-bind="title">Project details</h2>
<button class="lyra-drawer__close" x-bind="close">×</button>
</div>
<div class="lyra-drawer__body">Review the latest deployment before you continue.</div>
<div class="lyra-drawer__footer">
<button class="lyra-btn lyra-btn--primary lyra-btn--md" type="button" x-bind="close">
Close
</button>
</div>
</div>
</div>
</div><lyra:drawer> Gerado do lyra-ds/blade v0.10.0.
O comportamento vem de lyraDrawer() — instale @lyra-ds/alpine e veja a aba HTML + Alpine.
| Prop | Padrão | Obrigatória | Valores de exemplo |
|---|---|---|---|
title | — | Obrigatória | — |
closable | true | — | — |
closeLabel | 'Close' | — | — |
defaultOpen | false | — | — |
labelId | null | — | — |
<lyra:drawer title="Project settings" close-label="Close">
Rename the project, change its visibility or archive it.
</lyra:drawer>