You are looking at the documentation of the work in progress Hope UI 1.0, examples and information may be broken or outdated.

Overlays

Modal

Display a modal dialog.

Import

tsx
import { Modal } from "@hope-ui/core";
tsx
import { Modal } from "@hope-ui/core";
  • Modal: The wrapper that provides props, state, and context to its children.
  • Modal.Overlay: The backdrop behind the modal.
  • Modal.Content: The modal itself.
  • Modal.CloseButton: A button to close the modal.
  • Modal.Heading: An optional heading for the modal.
  • Modal.Description: An optional description for the modal.

Usage

tsx
function BasicUsageExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
</>
);
}
tsx
function BasicUsageExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
</>
);
}

Initial focus

When the modal opens, focus is automatically set on the first focusable element in Modal.Content.

Use the data-autofocus attribute on any element to mark it as the initial focus element.

tsx
function InitialFocusExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<Text mb={4}>The content of the Modal.</Text>
<HStack justifyContent="flex-end" spacing={4}>
<Button data-autofocus _focus={{ color: "red" }}>
Action
</Button>
</HStack>
</Modal.Content>
</Modal>
</>
);
}
tsx
function InitialFocusExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<Text mb={4}>The content of the Modal.</Text>
<HStack justifyContent="flex-end" spacing={4}>
<Button data-autofocus _focus={{ color: "red" }}>
Action
</Button>
</HStack>
</Modal.Content>
</Modal>
</>
);
}

You can also use a different css selector by using the initialFocusSelector prop.

tsx
function CustomInitialFocusExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal
isOpen={isOpen()}
onClose={() => setIsOpen(false)}
initialFocusSelector="#initial-focus"
>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<Text mb={4}>The content of the Modal.</Text>
<HStack justifyContent="flex-end" spacing={4}>
<Button id="initial-focus" _focus={{ color: "red" }}>
Action
</Button>
</HStack>
</Modal.Content>
</Modal>
</>
);
}
tsx
function CustomInitialFocusExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal
isOpen={isOpen()}
onClose={() => setIsOpen(false)}
initialFocusSelector="#initial-focus"
>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<Text mb={4}>The content of the Modal.</Text>
<HStack justifyContent="flex-end" spacing={4}>
<Button id="initial-focus" _focus={{ color: "red" }}>
Action
</Button>
</HStack>
</Modal.Content>
</Modal>
</>
);
}

If the modal contains no focusable element, focus is set to the Modal.Content itself.

Restore focus

When the modal closes, focus is set back to the trigger element. Use the restoreFocusSelector prop and pass in a css selector to target another element.

tsx
function CustomRestoreFocusExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<HStack spacing={4}>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal
isOpen={isOpen()}
onClose={() => setIsOpen(false)}
restoreFocusSelector="[data-finalfocus]"
>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
<Button data-finalfocus _focus={{ color: "red" }}>
Final focus element
</Button>
</HStack>
);
}
tsx
function CustomRestoreFocusExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<HStack spacing={4}>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal
isOpen={isOpen()}
onClose={() => setIsOpen(false)}
restoreFocusSelector="[data-finalfocus]"
>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
<Button data-finalfocus _focus={{ color: "red" }}>
Final focus element
</Button>
</HStack>
);
}

Focus trap

By default, focus is lock inside the modal for accessibility reason. Set the trapFocus prop to false if you want to disable this behavior.

tsx
function DisableFocusTrapExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)} trapFocus={false}>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
</>
);
}
tsx
function DisableFocusTrapExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)} trapFocus={false}>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
</>
);
}

Prevent scroll

For accessibility reason scrolling on the main document behind the modal is blocked by default. Set the preventScroll prop to false to allow scrolling behind the modal.

tsx
function DisablePreventScrollExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)} preventScroll={false}>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
</>
);
}
tsx
function DisablePreventScrollExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)} preventScroll={false}>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
</>
);
}

Heading and description

Use Modal.Heading to add an accessible heading to the modal, it renders an h2 by default.

Similarly Modal.Description is used add an accessible description and renders a p by default.

Under the hood, Hope UI will set the correct aria-labelledby and aria-describedby attributes.

tsx
function HeadingAndDescriptionExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack alignItems="flex-start" justifyContent="space-between" mb={4}>
<VStack alignItems="flex-start">
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.Description
fontSize="sm"
color={{ light: "neutral.600", dark: "neutral.300" }}
mb={4}
>
Description
</Modal.Description>
</VStack>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
</>
);
}
tsx
function HeadingAndDescriptionExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack alignItems="flex-start" justifyContent="space-between" mb={4}>
<VStack alignItems="flex-start">
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.Description
fontSize="sm"
color={{ light: "neutral.600", dark: "neutral.300" }}
mb={4}
>
Description
</Modal.Description>
</VStack>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
</>
);
}

Open/close transitions

The contentTransitionOptions and overlayTransitionOptions props allows to customize the modal content and overlay open/close transitions. You can use predefined transitions or create custom ones.

tsx
function TransitionExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal
isOpen={isOpen()}
onClose={() => setIsOpen(false)}
contentTransitionOptions={{
transition: "slide-up",
duration: 400,
exitDuration: 250,
easing: "ease-out",
exitEasing: "ease-in",
}}
>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
</>
);
}
tsx
function TransitionExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal
isOpen={isOpen()}
onClose={() => setIsOpen(false)}
contentTransitionOptions={{
transition: "slide-up",
duration: 400,
exitDuration: 250,
easing: "ease-out",
exitEasing: "ease-in",
}}
>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
</>
);
}

To learn more about the transition API check out the createTransition documentation.

Center modal vertically

Use the isCentered prop to vertically center the modal.

tsx
function VerticallyCenteredExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)} isCentered>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
</>
);
}
tsx
function VerticallyCenteredExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)} isCentered>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
</>
);
}

Size

You can change the modal width by setting the size prop.

tsx
function SizeExample() {
const [size, setSize] = createSignal<ModalProps["size"]>("md");
const [isOpen, setIsOpen] = createSignal(false);
const handleClick = (newSize: ModalProps["size"]) => {
setSize(newSize);
setIsOpen(true);
};
const sizes: Array<ModalProps["size"]> = ["xs", "sm", "md", "lg", "xl", "full"];
return (
<>
<HStack spacing={4}>
<For each={sizes}>{size => <Button onClick={() => handleClick(size)}>{size}</Button>}</For>
</HStack>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)} size={size()}>
<Show when={size() !== "full"}>
<Modal.Overlay />
</Show>
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
</>
);
}
tsx
function SizeExample() {
const [size, setSize] = createSignal<ModalProps["size"]>("md");
const [isOpen, setIsOpen] = createSignal(false);
const handleClick = (newSize: ModalProps["size"]) => {
setSize(newSize);
setIsOpen(true);
};
const sizes: Array<ModalProps["size"]> = ["xs", "sm", "md", "lg", "xl", "full"];
return (
<>
<HStack spacing={4}>
<For each={sizes}>{size => <Button onClick={() => handleClick(size)}>{size}</Button>}</For>
</HStack>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)} size={size()}>
<Show when={size() !== "full"}>
<Modal.Overlay />
</Show>
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
</>
);
}

Styling the backdrop

You can use style props and sx on the Modal.Overlay component to customize the modal backdrop.

tsx
function CustomBackdropExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Modal.Overlay
sx={{
bg: "blackAlpha.300",
backdropFilter: "blur(10px) hue-rotate(90deg)",
}}
/>
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
</>
);
}
tsx
function CustomBackdropExample() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Modal.Overlay
sx={{
bg: "blackAlpha.300",
backdropFilter: "blur(10px) hue-rotate(90deg)",
}}
/>
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<p>The content of the Modal.</p>
</Modal.Content>
</Modal>
</>
);
}

Please be aware that not every browser supports the backdrop-filter CSS property used in the example above.

Overflow behavior

If the content within the modal overflows beyond the viewport, you can use the scrollBehavior prop to control how scrolling should happen.

  • outside: overflow is handled by the modal wrapper.
  • inside: overflow is handled by the Modal.Content.
tsx
function ScrollBehaviorExample() {
const [scrollBehavior, setScrollBehavior] = createSignal<ModalProps["scrollBehavior"]>("outside");
const [isOpen, setIsOpen] = createSignal(false);
const handleClick = (newValue: ModalProps["scrollBehavior"]) => {
setScrollBehavior(newValue);
setIsOpen(true);
};
const scrollBehaviors: Array<ModalProps["scrollBehavior"]> = ["outside", "inside"];
return (
<>
<HStack spacing={4}>
<For each={scrollBehaviors}>
{scrollBehavior => (
<Button onClick={() => handleClick(scrollBehavior)}>{scrollBehavior} overflow</Button>
)}
</For>
</HStack>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)} scrollBehavior={scrollBehavior()}>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<For each={Array(100).fill("")}>{() => <p>The content of the Modal.</p>}</For>
</Modal.Content>
</Modal>
</>
);
}
tsx
function ScrollBehaviorExample() {
const [scrollBehavior, setScrollBehavior] = createSignal<ModalProps["scrollBehavior"]>("outside");
const [isOpen, setIsOpen] = createSignal(false);
const handleClick = (newValue: ModalProps["scrollBehavior"]) => {
setScrollBehavior(newValue);
setIsOpen(true);
};
const scrollBehaviors: Array<ModalProps["scrollBehavior"]> = ["outside", "inside"];
return (
<>
<HStack spacing={4}>
<For each={scrollBehaviors}>
{scrollBehavior => (
<Button onClick={() => handleClick(scrollBehavior)}>{scrollBehavior} overflow</Button>
)}
</For>
</HStack>
<Modal isOpen={isOpen()} onClose={() => setIsOpen(false)} scrollBehavior={scrollBehavior()}>
<Modal.Overlay />
<Modal.Content p={4}>
<HStack justifyContent="space-between" mb={4}>
<Modal.Heading fontWeight="semibold">Title</Modal.Heading>
<Modal.CloseButton />
</HStack>
<For each={Array(100).fill("")}>{() => <p>The content of the Modal.</p>}</For>
</Modal.Content>
</Modal>
</>
);
}

Accessibility

ARIA roles and attributes

  • The Modal.Content has aria-modal set to true.
  • The Modal.Content has aria-labelledby set to the id of the Modal.Heading, if any.
  • The Modal.Content has aria-describedby set to the id of the Modal.Description, if any.

Keyboard support and focus management

  • If the trapFocus prop is true, focus is locked within the modal.
  • If the initialFocusSelector prop is set, focus is moved to the matching element, otherwise focus moves to the Modal.Content.
  • When the modal closes, focus returns to the trigger element unless another focusable element takes focus.
  • If the closeOnOverlayClick prop is true, clicking on the overlay closes the modal.
  • When focus is within the Modal.Content and closeOnEsc prop is true, pressing esc closes the modal.
  • If the preventScroll prop is true, scrolling is blocked on the elements behind the modal.

Theming

Use the ModalTheme type to override Modal styles and default props when extending Hope UI theme.

tsx
import { extendTheme, ModalTheme } from "@hope-ui/core";
const theme = extendTheme({
components: {
Modal: {
// ...overrides
} as ModalTheme,
},
});
tsx
import { extendTheme, ModalTheme } from "@hope-ui/core";
const theme = extendTheme({
components: {
Modal: {
// ...overrides
} as ModalTheme,
},
});

Component parts

NameStatic selectorDescription
roothope-Modal-rootRoot element
contenthope-Modal-contentModal content
overlayhope-Modal-overlayModal overlay/backdrop
headinghope-Modal-headingModal heading
descriptionhope-Modal-descriptionModal description

Props

NameTypeDescriptionDefault value
isOpen*booleanWhether the modal should be shown.
onClose*() => voidA function that will be called to close the modal.
idstringThe id of the modal content.
size"xs" | "sm" | "md" | "lg" | "xl" | "full"The size of the modal.
isCenteredbooleanWhether the modal should be centered on screen.
scrollBehavior"inside" | "outside"Defines how scrolling should happen when content overflows beyond the viewport.
contentTransitionOptionsTransitionOptionsOptions passed to the modal content transition.
overlayTransitionOptionsTransitionOptionsOptions passed to the overlay transition.
closeOnOverlayClickbooleanWhether the modal should close when the overlay is clicked.true
closeOnEscbooleanWhether the modal should close when the user hit the Esc key.true
preventScrollbooleanWhether the scroll should be disabled on the body when the modal opens.true
trapFocusbooleanWhether the focus will be locked into the modal.true
initialFocusSelectorstringA query selector to retrieve the element that should receive focus once the modal opens.
restoreFocusSelectorstringA query selector to retrieve the element that should receive focus once the modal closes.
onOverlayClick() => voidA function that will be called when the overlay is clicked.
onEscKeyDown() => voidA function that will be called when the Esc key is pressed and focus is within the modal.

Other components props

  • Modal.CloseButton renders a CloseButton and supports all of its props.
  • Modal.Overlay, Modal.Content, Modal.Heading and Modal.Description supports commons Hope UI props (style props, sx and as).
Previous
Drawer