This article has been machine-translated from Chinese. The translation may contain inaccuracies or awkward phrasing. If in doubt, please refer to the original Chinese version.
Previous article: Floating UI Practical Guide - Popover
In this article, I’ll share how to use Floating UI to create another common floating UI component — Dialog. A Dialog is a floating element that displays information requiring immediate attention. It appears over the page content and blocks interaction with the page until it is dismissed.
It has similar interactions to a popover, but with two key differences:
- It is modal and renders a backdrop behind the dialog, dimming the background content and making the rest of the page inaccessible.
- It is centered in the viewport and not anchored to any specific reference element.
An accessible dialog component has the following key points:
Dismissal: It closes when the user presses theesckey or clicks outside the open dialog.Role: The element is given relevant roles and ARIA attributes so screen readers can access it.Focus management: Focus is completely trapped within the dialog and must be dismissed by the user.
Target Component
Goal: Implement a Dialog Demo like this:
Next, we need to create a React component called Dialog that uses the @floating-ui/react library to create an interactive floating dialog. Here’s the design for the component:
Component Props
The Dialog component needs to accept the following props:
rootId: The root element for the floating element, optional.open: A boolean controlling whether the dialog is open.initialOpen: Whether the dialog is initially open, defaults tofalse.onOpenChange: A callback function called when the dialog’s open state changes, accepting a boolean parameter.render: A function that accepts an object parameter containing aclosemethod for closing the dialog. This function returns the React node to render inside the dialog.className: CSS class name applied to the dialog.overlayClass: CSS class name applied to the floating overlay.containerClass: CSS class name applied to the dialog container.isDismiss: A boolean determining whether clicking outside the dialog closes it, defaults totrue.children: React child elements, can be a button that opens the dialog when clicked.showCloseButton: A boolean determining whether to show a close button, defaults totrue.
Component Functionality
The main functionality of the Dialog component is to create an interactive floating dialog that can be closed by clicking the close button or clicking outside the dialog area. The dialog’s open and close state can be controlled via the open and onOpenChange props (controlled mode), or managed automatically via internal state (uncontrolled mode).
The Dialog component uses several hooks from the @floating-ui/react library:
useFloating: Used to manage the dialog’s open and close state.useClick,useRole, anduseDismiss: Used to handle dialog interactions such as clicks and role management.useInteractions: Used to get and set interaction properties.
Additionally, the Dialog component uses FloatingPortal, FloatingOverlay, and FloatingFocusManager components to create the floating dialog UI.
Complete Code
Combining all of the above, we can write a fairly complete Dialog example that supports custom overlay styling, inner element styling, control over whether clicking the overlay closes the dialog, and can even be combined with Framer Motion for dialog animations (I’ll write an article about that if I get the chance).
import {
FloatingFocusManager,
FloatingOverlay,
FloatingPortal,
useClick,
useDismiss,
useFloating,
useInteractions,
useRole,
} from '@floating-ui/react';
import clsx from 'clsx';
import React, { cloneElement, useState } from 'react';
import { CgClose } from 'react-icons6GH))S[9A2G57O0%MM45V.gif)';
type DialogProps = {
rootId?: string;
open?: boolean;
initialOpen?: boolean;
onOpenChange?: (open: boolean) => void;
children?: JSX.Element;
render: (props: { close: () => void }) => React.ReactNode;
className?: string;
overlayClass?: string;
containerClass?: string;
isDismiss?: boolean;
showCloseButton?: boolean;
};
export default function Dialog({
initialOpen = false,
open: controlledOpen,
onOpenChange: setControlledOpen,
children,
className,
render,
rootId: customRootId,
overlayClass,
containerClass,
showCloseButton = true,
isDismiss = true,
}: DialogProps) {
const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen);
const open = controlledOpen ?? uncontrolledOpen;
const setOpen = setControlledOpen ?? setUncontrolledOpen;
const { reference, floating, context } = useFloating({
open,
onOpenChange: setOpen,
});
const click = useClick(context);
const role = useRole(context);
const dismiss = useDismiss(context, { enabled: isDismiss, outsidePressEvent: 'mousedown' });
const { getReferenceProps, getFloatingProps } = useInteractions([click, role, dismiss]);
const onClose = () => setOpen(false);
return (
<>
{children && cloneElement(children, getReferenceProps({ ref: reference, ...children.props }))}
<FloatingPortal id={customRootId}>
{open && (
<FloatingOverlay
className={clsx('absolute inset-0 z-10 flex h-full w-full items-center', overlayClass ?? 'bg-black/60')}
lockScroll
>
<div className={clsx('m-auto grid place-items-center', containerClass)}>
<FloatingFocusManager context={context}>
<div
className={clsx('relative overflow-hidden rounded-md bg-white', className ?? 'mx-24')}
ref={floating}
{...getFloatingProps()}
>
{showCloseButton && <CgClose className="absolute right-2 top-2 h-6 w-6 cursor-pointer" onClick={onClose} />}
{render({ close: onClose })}
</div>
</FloatingFocusManager>
</div>
</FloatingOverlay>
)}
</FloatingPortal>
</>
);
}
Basic Dialog Hooks
Official example: CodeSandbox demo
This example demonstrates how to create a dialog for a single instance to familiarize yourself with the basics.
Let’s look at this example:
Open state
import { useState } from 'react';
function Dialog() {
const [isOpen, setIsOpen] = useState(false);
}
isOpen determines whether the dialog is currently open on the screen. It is used for conditional rendering.
useFloating hook
The useFloating() hook provides context for our dialog. We need to pass some information:
open: The open state from ouruseState()hook above.onOpenChange: A callback function called when the dialog opens or closes. We’ll use it to update ourisOpenstate.
import { useFloating } from '@floating-ui/react';
function Dialog() {
const [isOpen, setIsOpen] = useState(false);
const { refs, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
});
}
Interaction hooks
useClick()adds the ability to open or close the dialog when clicking the reference element. However, a dialog may not be attached to a reference element, so this is optional. (Typically dialogs are portaled independently, meaning the context is the body.)useDismiss()adds the ability to close the dialog when the user pressesescor clicks outside the dialog. ItsoutsidePressEventoption can be set to'mousedown'so that touch events become lazy and don’t pass through the backdrop, since the default behavior is eager.useRole()adds the correct ARIA attributes fordialogto the dialog and reference elements.
Finally, useInteractions() merges all their props into prop getters, which can then be used for rendering.
import {
// ...
useClick,
useDismiss,
useRole,
useInteractions,
useId,
} from '@floating-ui/react';
function Dialog() {
const [isOpen, setIsOpen] = useState(false);
const { refs, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
});
const click = useClick(context);
const dismiss = useDismiss(context, {
outsidePressEvent: 'mousedown',
});
const role = useRole(context);
// Merge all the interactions into prop getters
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]);
// Set up label and description ids
const labelId = useId();
const descriptionId = useId();
}
Rendering
Now that we’ve set up all our variables and hooks, we can render our elements.
function Dialog() {
// ...
return (
<>
<button ref={refs.setReference} {...getReferenceProps()}>
Reference element
</button>
{isOpen && (
<FloatingOverlay lockScroll style={{ background: 'rgba(0, 0, 0, 0.8)' }}>
<FloatingFocusManager context={context}>
<div ref={refs.setFloating} aria-labelledby={labelId} aria-describedby={descriptionId} {...getFloatingProps()}>
<h2 id={labelId}>Heading element</h2>
<p id={descriptionId}>Description element</p>
<button onClick={() => setIsOpen(false)}>Close</button>
</div>
</FloatingFocusManager>
</FloatingOverlay>
)}
</>
);
}
/{...getFloatingProps()}As mentioned in the previous article, these spread props from the interaction hooks onto the relevant elements. They include props such asonClick,aria-expanded, etc.
FloatingPortal & FloatingOverlay & FloatingFocusManager
<FloatingOverlay />is a component that renders a backdrop overlay element behind the floating element, with the ability to lock body scrolling. FloatingOverlay docs- Provides a fixed base style that dims the background content and blocks pointer events behind the floating element.
- It’s a regular
<div/>, so it can be styled with any CSS solution.
<FloatingFocusManager />- FloatingPortal docs A component that manages and controls focus for floating elements on the page.
- Automatically detects focus changes, adjusts the position and state of floating elements on the page, ensuring accessibility and usability of all elements.
- By default, it typically traps focus inside.
- It should directly wrap the floating element and only be rendered when the dialog is also rendered. FloatingFocusManager docs
<FloatingPortal />teleports the floating element into a given container element — by default, outside the application root and into the body.- The root can be customized, meaning you can select a node with an id, or create one and append it to the specified root (body).
import { FloatingPortal } from '@floating-ui/react';
function Tooltip() {
return (
isOpen && (
<FloatingPortal>
<div>Floating element</div>
</FloatingPortal>
)
);
}
喜欢的话,留下你的评论吧~