Previous article: Floating UI Experience Sharing - Popover
In this article, I will share how to use Floating UI to create another common floating UI component—Dialog. A Dialog is a floating element that displays information that requires immediate attention; it appears over the page content and prevents interaction with the page until it is closed.
It has similar interactions to popups, but there are two main differences:
- It is modal and presents a background behind the dialog, dimming the content behind it and making the rest of the page inaccessible.
- It is centered in the viewport and is not anchored to any specific reference element.
An accessible dialog component has the following key points:
Dismissal
: It closes when the user presses theesc
key or clicks outside the open dialog.Role
: The element is assigned relevant roles and ARIA attributes so that screen readers can access it.Focus management
: Focus is completely trapped within the dialog and must be released by the user.
Target Component#
Goal: Implement a Dialog Demo 👇
Next, we need to create a React component named Dialog
, which uses the @floating-ui/react
library to create an interactive floating dialog. Here is the concept for the component:
Component Parameters#
The Dialog
component needs to accept the following parameters:
rootId
: The root element of the floating element, optional.open
: A boolean that controls whether the dialog is open.initialOpen
: A boolean indicating whether the dialog is initially open, default isfalse
.onOpenChange
: A callback function that is called when the dialog's open state changes, accepting a boolean parameter.render
: A function that accepts an object parameter containing aclose
method to close the dialog. This function returns the React node to be rendered in 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 that determines whether to enable the feature of closing the dialog by clicking outside, default istrue
.children
: React child elements, which can be a button that opens the dialog when clicked.showCloseButton
: A boolean that determines whether to show the close button, default istrue
.
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. The open and close state of the dialog can be controlled through the open
and onOpenChange
parameters (controlled), or it can be managed automatically through internal state (uncontrolled).
The Dialog
component uses several hooks from the @floating-ui/react
library:
useFloating
: Used to manage the open and close state of the dialog.useClick
,useRole
, anduseDismiss
: Used to handle interactions with the dialog, such as clicks and role management.useInteractions
: Used to get and set interaction properties.
Additionally, the Dialog
component also uses FloatingPortal
, FloatingOverlay
, and FloatingFocusManager
components to create the UI for the floating dialog.
Complete Code#
Combining practical aspects, we can write a more complete Dialog example that allows customization of the overlay, internal element styles, controls whether clicking the overlay closes the dialog, and can incorporate animations using Framer-motion (I may write an article about this in the future).
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-icons/cg';
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 take a 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 will use it to update ourisOpen
state.
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, the dialog may not be attached to the reference element, so this is optional. (Generally, dialogs are separated into a Portal, meaning the context is the body.)useDismiss()
adds the ability to close the dialog when the user presses theesc
key or clicks outside the dialog. You can set itsoutsidePressEvent
option to'mousedown'
so that touch events become lazy and do not pass through the background, as the default behavior is eager. (This is a bit hard to understand, but that's the gist.)useRole()
adds the correct ARIA attributes fordialog
to 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 have set up all the 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>
)}
</>
);
}
{...getReferenceProps()}
/{...getFloatingProps()}
as mentioned in the previous article, spreads props from interaction hooks to the relevant elements. They contain props such asonClick
,aria-expanded
, etc.
FloatingPortal & FloatingOverlay & FloatingFocusManager#
<FloatingOverlay />
is a component that renders a background overlay element behind the floating element, with the ability to lock body scroll. FloatingOverlay docs- It provides a fixed basic style that dims the background content and prevents pointer events behind the floating element.
- It is a regular
<div/>
, so it can be styled using any CSS solution.
<FloatingFocusManager />
- FloatingPortal docs is a component that manages and controls the focus of floating elements on the page.
- It automatically detects focus changes, adjusts the position and state of floating elements on the page, ensuring accessibility and usability for all elements on the page.
- By default, it usually captures focus internally.
- It should directly wrap the floating element and should only be rendered when the dialog is also rendered. FloatingFocusManager docs
<FloatingPortal />
transports the floating element to a given container element—by default, outside the application root and into the body.- You can customize the root, meaning you can choose a node with an id, or create it and attach it to a specified root (body).
import {FloatingPortal} from '@floating-ui/react';
function Tooltip() {
return (
isOpen && (
<FloatingPortal>
<div>Floating element</div>
</FloatingPortal>
)
);
}