import {
Meta,
Story,
ArgTypes,
Controls,
Source,
} from '@storybook/addon-docs/blocks';
Dismiss
The @bento/dismiss package provides an accessible, visually hidden dismissal
control for overlays and popup content. It ensures users, especially those using
screen readers, can reliably dismiss modal dialogs, drawers, and popovers
through linear keyboard navigation. This component complements ESC key handling
and outside-click behaviors, filling a critical accessibility gap when sighted
users have a visible close button but screen reader users navigating linearly
encounter no dismissal affordance.
This primitive aligns with React Aria's DismissButton pattern and should be
positioned at both the start and end of dismissible overlay content. When a
screen reader user navigates forward or backward through an overlay, they will
encounter these controls, giving them a clear way to exit the overlay without
relying solely on the ESC key or outside-click patterns that may not be
discoverable through linear navigation.
Installation
npm install --save @bento/dismiss
Props
The @bento/dismiss package exports the Dismiss component:
import { Dismiss } from '@bento/dismiss';
<Dismiss onDismiss={() => setOpen(false)} />
The following properties are available to be used on the Dismiss component:
onDismiss | () => void | No | Called when the dismiss button is activated. |
ariaLabel | string | No | Accessible label for the dismiss button. Should be localized. |
children | ReactNode | No | The content to render inside the container. |
slots | { hidden?: ((args: { props: Record<string, unknown>; children: ReactNode; }) => ReactNode) | Record<string, unknown>; } & Record<string, object | Function> | No | Optional slot to customize the VisuallyHidden wrapper. |
| An object that contains the customizations for the slots. | | | |
| The main way you interact with the slot system as a consumer. | | | |
slot | string | No | A named part of a component that can be customized. This is implemented by the consuming component. |
| The exposed slot names of a component are available in the components documentation. | | | |
For all other properties specified on the Dismiss component, the component
will pass them down to the underlying button element. This includes properties
such as id, data-* attributes, or additional ARIA attributes that you might
need for specialized use cases.
Example
<Source language='tsx' code={ SourceBasic } />
Examples
Basic Usage
The most common pattern for the Dismiss component is to place it at both the
start and end of dismissible overlay content. This ensures screen reader users
navigating forward encounter a dismissal control at the beginning, and users
navigating backward from content below the overlay encounter a dismissal control
at the end.
<Source language='tsx' code={ SourceBasic } />
In this example, when a screen reader user navigates into the dialog, they
immediately encounter the first dismiss button. If they navigate through all the
content and continue forward, they encounter the second dismiss button before
exiting the overlay's focus boundary. Both buttons invoke the same onDismiss
callback to close the dialog.
Custom Label
The default accessible label is "Dismiss". For localization or context-specific
labeling, use the ariaLabel prop to provide a custom label that makes sense in
your application's language and terminology.
<Source language='tsx' code={ SourceCustomLabel } />
Providing clear, localized labels helps screen reader users understand what will
happen when they activate the dismiss button. Use labels that match the
terminology of your application, such as "Close dialog", "Exit menu", or
"Dismiss notification".
Usage Guidelines
Understanding when and where to use the Dismiss component is critical for
creating accessible overlay patterns. The following guidance is based on React
Aria patterns and WCAG accessibility standards.
Required Usage
The Dismiss component is required in the following contexts:
Modal Dialogs and Overlays
When creating modal dialogs that block interaction with the rest of the page,
place a Dismiss control at the start and end of the dialog content. This
provides screen reader users with a reliable dismissal mechanism regardless of
whether they navigate forward or backward through the content. While sighted
users can click a visible close button, screen reader users navigating linearly
need an equivalent affordance at both boundaries.
Modal Drawers and Sheets
Side panels and bottom sheets that use modal behavior should follow the same
pattern as modal dialogs. Position dismiss controls at the boundaries of the
drawer content to ensure users can exit through linear navigation.
Recommended Usage
The Dismiss component is recommended but not strictly required in these
contexts:
Dismissible Popovers and Menus
When popovers or dropdown menus can be dismissed but lack a visible close
button, include start and end dismiss controls. This ensures screen reader users
can exit the popup even when ESC key handling might not be discoverable.
Coachmarks and Tour Steps
Product tours and onboarding flows that can be dismissed should include dismiss
controls. If the tour requires explicit action buttons to proceed, consider
whether a generic dismiss is appropriate or whether users should be guided
through specific actions.
Combobox and Select Popovers
While combobox popovers typically manage focus automatically, adding dismiss
controls can help screen reader users who want to exit the popup without making
a selection.
Not Recommended
Do not use the Dismiss component in these contexts:
Tooltips
Tooltips are non-interactive content that appear on hover or focus and do not
trap focus. They do not need dismiss controls as users can simply navigate away
from the trigger element.
Toasts and Notifications
Toast notifications and growl-style messages typically do not trap focus and
should not use the Dismiss component. If dismissal is needed, provide a
visible close button instead.
Non-Dismissible Content
Legal consent dialogs, age gates, or other content that requires explicit user
action should use specific action buttons rather than a generic dismiss control.
Placement Rules
Follow these guidelines for correct placement:
Position the first dismiss control immediately after the opening tag of your
overlay content container, before any other focusable elements. Position the
second dismiss control immediately before the closing tag of your overlay
content container, after all other focusable elements. This ensures linear
navigation always encounters a dismiss affordance at the boundaries.
Always maintain a visible close button for sighted users. The Dismiss
component is a screen reader affordance, not a replacement for visible UI
controls. Sighted keyboard users and mouse users need their own clear way to
close overlays.
Use meaningful, localized labels through the ariaLabel prop. The default
"Dismiss" may not be appropriate for all contexts or languages. Consider labels
like "Close dialog", "Exit menu", or region-specific translations that match
your application's terminology.
Accessibility
The Dismiss component is designed with accessibility as its primary purpose.
It fills a specific gap in overlay accessibility by providing a dismissal
affordance for users navigating with screen readers using linear navigation
patterns.
Screen Reader Compatibility
The component uses the @bento/visually-hidden primitive to ensure the button
remains completely accessible to assistive technologies while being visually
hidden from sighted users. This technique uses CSS to position content
off-screen rather than using display: none or visibility: hidden, which
would make the content unavailable to screen readers.
Keyboard Navigation
The dismiss button is fully keyboard accessible. Screen reader users can
activate it using Enter or Space when focused on the element. The component uses
tabIndex={-1} which removes the button from the standard tab order but allows
screen readers to navigate to it through virtual cursor navigation. This is the
standard pattern for this type of control and aligns with React Aria's
DismissButton implementation.
ARIA Labeling
The component applies aria-label to provide context about what the button
does. The default label is "Dismiss", but you should customize this through the
ariaLabel prop to match your application's terminology and language. Clear,
descriptive labels help screen reader users understand the button's purpose
without requiring additional context.
Semantic HTML
The component renders as a native HTML button element with type="button" to
prevent accidental form submission. Using semantic HTML ensures compatibility
with assistive technologies and provides the expected button semantics and
keyboard behavior automatically.
Integration with Overlay Patterns
The Dismiss component is designed to work alongside other accessibility
features in overlay patterns. It complements ESC key handling from React Aria's
overlay hooks, outside-click dismissal through OverlayTrigger, and focus
containment from focus management utilities. While these features provide
dismissal mechanisms for some users, the Dismiss component specifically
addresses the needs of screen reader users navigating linearly through content.
Position the component inside your overlay's focus boundary, within the same
container as your overlay content. It should be rendered before and after your
overlay's main content to ensure users encounter it when navigating in either
direction.
Customization
The Dismiss component is built using the @bento/slots package, allowing you
to customize specific parts of the component through slot-based composition.
While the component is intentionally visually hidden, understanding its
customization options can be useful for specialized use cases or debugging.
Slots
The component is registered as BentoDismiss and introduces the following slots:
hidden: Assigned to the @bento/visually-hidden component that wraps the dismiss button.
You can use the slots prop to override the default behavior of the visually hidden wrapper:
<Source language='tsx' code={ SourceSlotCustomization } />
In this example, we override the hidden slot to provide custom styling to the
visually hidden wrapper. While this is possible, it is rarely necessary in
practice since the component is designed to be visually hidden by default.
See the @bento/slots package for more information on how to use the slot and
slots properties.
Styling
While the dismiss button is visually hidden by default and styling is typically
not necessary, you can customize the underlying button element using className
or style props. These props will be applied to the button element itself, not
the visually hidden wrapper.
import { Dismiss } from '@bento/dismiss';
<Dismiss onDismiss={handleDismiss} className="my-dismiss-button" />
When you assign a className to the component, you take full responsibility for
styling. The component will pass this className to the button element, allowing
you to target it with CSS selectors. However, keep in mind that because the
button is wrapped in a visually hidden container, visual styles will not be
visible to sighted users.
Data Attributes
The following data attributes are automatically applied to the component:
data-hidden | Applied to the visually hidden wrapper, indicating content is accessible to screen readers | "true" |
These attributes are provided by the underlying @bento/visually-hidden
component and can be used for debugging or specialized styling scenarios:
[data-hidden="true"] button[type="button"] {
}
Default Attributes
The component renders a native button element with the following attributes:
type="button": Prevents the button from submitting forms when used inside
form elements.
tabIndex={-1}: Removes the button from the standard tab order while keeping
it accessible to screen readers through virtual cursor navigation.
aria-label: Provides an accessible label for screen readers. Defaults to
"Dismiss" but can be customized via the ariaLabel prop.
style={{ width: 1, height: 1 }}: A defensive fallback that ensures the button
has minimal dimensions even when visually hidden, which can help with screen
reader detection in certain scenarios.