Skip to content

Modal

A dialog over the page, HeroUI's on Kobalte's.

Solidtype_heroui.Modal is Kobalte’s Dialog, with the classes HeroUI’s own modalVariants gives. A Button inside it is its trigger, as in HeroUI; anything else is wrapped in Modal.Trigger. A Button with slot=`close inside the dialog closes it.

modal_usage.mlx
module Button = Solidtype_heroui.Button
module Modal = Solidtype_heroui.Modal
let make () =
<Modal>
<Button variant=`secondary>"Open Modal"</Button>
<Modal.Backdrop>
<Modal.Container>
<Modal.Dialog class_="sm:max-w-[360px]">
<Modal.Close_trigger />
<Modal.Header>
<Modal.Icon class_="bg-default text-foreground">
<Gravity.Rocket class_="size-5" />
</Modal.Icon>
<Modal.Heading>"Welcome to HeroUI"</Modal.Heading>
</Modal.Header>
<Modal.Body>
<p>
"A beautiful, fast, and modern React UI library for building \
accessible and customizable web applications with ease."
</p>
</Modal.Body>
<Modal.Footer>
<Button class_="w-full" slot=`close>"Continue"</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>
<Modal>
<Button>"Open Modal"</Button>
<Modal.Backdrop>
<Modal.Container>
<Modal.Dialog>
<Modal.Close_trigger />
<Modal.Header>
<Modal.Icon>...</Modal.Icon>
<Modal.Heading>...</Modal.Heading>
</Modal.Header>
<Modal.Body>...</Modal.Body>
<Modal.Footer>...</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>
modal_sizes.mlx
module Jsx = Solidtype.Jsx
module For = Solidtype.For
module Button = Solidtype_heroui.Button
module Modal = Solidtype_heroui.Modal
let sizes =
[|
(`xs, "xs");
(`sm, "sm");
(`md, "md");
(`lg, "lg");
(`cover, "cover");
(`full, "full");
|]
let make () =
<div class_="flex flex-wrap gap-4">
<For each=(fun () -> sizes)>
(fun (size, name) _ ->
<Modal>
<Button variant=`secondary>
(fun () -> Jsx.string (String.capitalize_ascii name))
</Button>
<Modal.Backdrop>
<Modal.Container size=(fun () -> size)>
<Modal.Dialog>
<Modal.Close_trigger />
<Modal.Header>
<Modal.Icon class_="bg-default text-foreground">
<Gravity.Rocket class_="size-5" />
</Modal.Icon>
<Modal.Heading>
"Size: "
(fun () -> Jsx.string (String.capitalize_ascii name))
</Modal.Heading>
</Modal.Header>
<Modal.Body>
<p>
"This modal uses the "
<code>(fun () -> Jsx.string name)</code>
(fun () ->
Jsx.string
(match size with
| `cover ->
" size variant. It spans the full screen with \
margins: 16px on mobile and 40px on desktop. \
Maintains rounded corners and standard padding. \
Perfect for cover-style content that needs \
maximum width while preserving modal aesthetics."
| `full ->
" size variant. It occupies the entire viewport \
without any margins, rounded corners, or shadows, \
creating a true fullscreen experience. Ideal for \
immersive content or full-page interactions."
| `xs | `sm | `md | `lg ->
" size variant. On mobile devices, all sizes adapt \
to near full-width for optimal viewing. On \
desktop, each size provides a different maximum \
width to suit various content needs."))
</p>
</Modal.Body>
<Modal.Footer>
<Button slot=`close variant=`secondary>"Cancel"</Button>
<Button slot=`close>"Confirm"</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>)
</For>
</div>
modal_placement.mlx
module Jsx = Solidtype.Jsx
module For = Solidtype.For
module Button = Solidtype_heroui.Button
module Modal = Solidtype_heroui.Modal
let placements =
[| (`auto, "auto"); (`top, "top"); (`center, "center"); (`bottom, "bottom") |]
let make () =
<div class_="flex flex-wrap gap-4">
<For each=(fun () -> placements)>
(fun (placement, name) _ ->
<Modal>
<Button variant=`secondary>
(fun () -> Jsx.string (String.capitalize_ascii name))
</Button>
<Modal.Backdrop>
<Modal.Container placement=(fun () -> placement)>
<Modal.Dialog class_="sm:max-w-[360px]">
<Modal.Close_trigger />
<Modal.Header>
<Modal.Icon class_="bg-default text-foreground">
<Gravity.Rocket class_="size-5" />
</Modal.Icon>
<Modal.Heading>
"Placement: "
(fun () -> Jsx.string (String.capitalize_ascii name))
</Modal.Heading>
</Modal.Header>
<Modal.Body>
<p>
"This modal uses the "
<code>(fun () -> Jsx.string name)</code>
" placement option. Try different placements to see how \
the modal positions itself on the screen."
</p>
</Modal.Body>
<Modal.Footer>
<Button class_="w-full" slot=`close>"Continue"</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>)
</For>
</div>
modal_scroll.mlx
module S = Solidtype.Solid
module Jsx = Solidtype.Jsx
module For = Solidtype.For
module Button = Solidtype_heroui.Button
module Modal = Solidtype_heroui.Modal
module Radio = Solidtype_heroui.Radio
module Radio_group = Solidtype_heroui.Radio_group
let paragraphs = Array.init 30 (fun i -> i + 1)
let make () =
let scroll, set_scroll = S.create_signal "inside" in
let label () = String.capitalize_ascii (S.get scroll) in
<div class_="flex flex-col gap-4">
<Radio_group
orientation=`horizontal
value=(fun () -> S.get scroll)
on_change=(S.set set_scroll)>
<Radio value="inside">
<Radio.Content>
<Radio.Control><Radio.Indicator /></Radio.Control> "Inside"
</Radio.Content>
</Radio>
<Radio value="outside">
<Radio.Content>
<Radio.Control><Radio.Indicator /></Radio.Control> "Outside"
</Radio.Content>
</Radio>
</Radio_group>
<Modal>
<Button variant=`secondary>
"Open Modal (" (fun () -> Jsx.string (label ())) ")"
</Button>
<Modal.Backdrop>
<Modal.Container
scroll=(fun () ->
match S.get scroll with
| "outside" -> `outside
| _ -> `inside)>
<Modal.Dialog class_="sm:max-w-[360px]">
<Modal.Header>
<Modal.Heading>
"Scroll: " (fun () -> Jsx.string (label ()))
</Modal.Heading>
<p class_="text-sm leading-5 text-muted">
"Compare scroll behaviors - inside keeps content scrollable \
within the modal, outside allows page scrolling"
</p>
</Modal.Header>
<Modal.Body>
<For each=(fun () -> paragraphs)>
(fun n _ ->
<p class_="mb-3">
"Paragraph "
(fun () -> Jsx.int n)
": Lorem ipsum dolor sit amet, consectetur adipiscing \
elit. Nullam pulvinar risus non risus hendrerit \
venenatis. Pellentesque sit amet hendrerit risus, sed \
porttitor quam."
</p>)
</For>
</Modal.Body>
<Modal.Footer>
<Button slot=`close variant=`secondary>"Cancel"</Button>
<Button slot=`close>"Confirm"</Button>
</Modal.Footer>
<Modal.Close_trigger />
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>
</div>

A signal is Solid’s state: each half keeps the modal’s open state in one, where HeroUI’s uses React.useState and useOverlayState.

With a signal

Control the modal using Solid's signal for simple state management. Perfect for basic use cases.

Status: closed

With a signal's setter

Use the signal's setter for a cleaner API with convenient calls like S.set set_open true, S.set set_open false, and S.update set_open not.

Status: closed

modal_controlled.mlx
module S = Solidtype.Solid
module Jsx = Solidtype.Jsx
module Button = Solidtype_heroui.Button
module Modal = Solidtype_heroui.Modal
(* HeroUI shows React's useState and its own useOverlayState; a signal is
Solid's state, so both halves keep the modal's in one. *)
let make () =
let is_open, set_open = S.create_signal false in
let state_open, set_state_open = S.create_signal false in
<div class_="flex max-w-md flex-col gap-8">
<div class_="flex flex-col gap-3">
<h3 class_="text-lg font-semibold text-foreground">"With a signal"</h3>
<p class_="text-sm leading-relaxed text-pretty text-muted">
"Control the modal using Solid's "
<code class_="text-foreground">"signal"</code>
" for simple state management. Perfect for basic use cases."
</p>
<div
class_="flex flex-col items-start gap-3 rounded-2xl bg-surface p-4 \
shadow-sm">
<div class_="flex w-full items-center justify-between">
<p class_="text-xs text-muted">
"Status: "
<span class_="font-mono font-medium text-foreground">
(fun () ->
Jsx.string (if S.get is_open then "open" else "closed"))
</span>
</p>
</div>
<div class_="flex gap-2">
<Button
size=`sm variant=`secondary on_click=(fun _ -> S.set set_open true)>
"Open Modal"
</Button>
<Button
size=`sm variant=`tertiary on_click=(fun _ -> S.update set_open not)>
"Toggle"
</Button>
</div>
</div>
<Modal.Backdrop
is_open=(fun () -> S.get is_open) on_open_change=(S.set set_open)>
<Modal.Container>
<Modal.Dialog class_="sm:max-w-[360px]">
<Modal.Close_trigger />
<Modal.Header>
<Modal.Icon class_="bg-accent-soft text-accent-soft-foreground">
<Gravity.Circle_check class_="size-5" />
</Modal.Icon>
<Modal.Heading>"Controlled with a signal"</Modal.Heading>
</Modal.Header>
<Modal.Body>
<p>
"This modal is controlled by Solid's "
<code>"signal"</code>
". Pass "
<code>"is_open"</code>
" and "
<code>"on_open_change"</code>
" props to manage the modal state externally."
</p>
</Modal.Body>
<Modal.Footer>
<Button slot=`close variant=`secondary>"Cancel"</Button>
<Button slot=`close>"Confirm"</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</div>
<div class_="flex flex-col gap-3">
<h3 class_="text-lg font-semibold text-foreground">
"With a signal's setter"
</h3>
<p class_="text-sm leading-relaxed text-pretty text-muted">
"Use the "
<code class_="text-foreground">"signal"</code>
"'s setter for a cleaner API with convenient calls like "
<code>"S.set set_open true"</code>
", "
<code>"S.set set_open false"</code>
", and "
<code>"S.update set_open not"</code>
"."
</p>
<div
class_="flex flex-col items-start gap-3 rounded-2xl bg-surface p-4 \
shadow-sm">
<div class_="flex w-full items-center justify-between">
<p class_="text-xs text-muted">
"Status: "
<span class_="font-mono font-medium text-foreground">
(fun () ->
Jsx.string (if S.get state_open then "open" else "closed"))
</span>
</p>
</div>
<div class_="flex gap-2">
<Button
size=`sm
variant=`secondary
on_click=(fun _ -> S.set set_state_open true)>
"Open Modal"
</Button>
<Button
size=`sm
variant=`tertiary
on_click=(fun _ -> S.update set_state_open not)>
"Toggle"
</Button>
</div>
</div>
<Modal.Backdrop
is_open=(fun () -> S.get state_open)
on_open_change=(S.set set_state_open)>
<Modal.Container>
<Modal.Dialog class_="sm:max-w-[360px]">
<Modal.Close_trigger />
<Modal.Header>
<Modal.Icon class_="bg-success-soft text-success-soft-foreground">
<Gravity.Circle_check class_="size-5" />
</Modal.Icon>
<Modal.Heading>"Controlled with a signal's setter"</Modal.Heading>
</Modal.Header>
<Modal.Body>
<p>
"The "
<code>"signal"</code>
{js|'s setter covers the common operations. No need to manually create callbacks—just use |js}
<code>"S.set set_open true"</code>
", "
<code>"S.set set_open false"</code>
", or "
<code>"S.update set_open not"</code>
"."
</p>
</Modal.Body>
<Modal.Footer>
<Button slot=`close variant=`secondary>"Cancel"</Button>
<Button slot=`close>"Confirm"</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</div>
</div>
modal_form.mlx
module Button = Solidtype_heroui.Button
module Input = Solidtype_heroui.Input
module Label = Solidtype_heroui.Label
module Modal = Solidtype_heroui.Modal
module Surface = Solidtype_heroui.Surface
module Text_field = Solidtype_heroui.Text_field
let make () =
<Modal>
<Button variant=`secondary>"Open Contact Form"</Button>
<Modal.Backdrop>
<Modal.Container placement=`auto>
<Modal.Dialog class_="sm:max-w-md">
<Modal.Close_trigger />
<Modal.Header>
<Modal.Icon class_="bg-accent-soft text-accent-soft-foreground">
<Gravity.Envelope class_="size-5" />
</Modal.Icon>
<Modal.Heading>"Contact Us"</Modal.Heading>
<p class_="mt-1.5 text-sm leading-5 text-muted">
"Fill out the form below and we'll get back to you. The modal \
adapts automatically when the keyboard appears on mobile."
</p>
</Modal.Header>
<Modal.Body class_="p-6">
<Surface variant=`default>
<form class_="flex flex-col gap-4">
<Text_field class_="w-full" name="name" variant=`secondary>
<Label>"Name"</Label>
<Input type_="text" placeholder="Enter your name" />
</Text_field>
<Text_field class_="w-full" name="email" variant=`secondary>
<Label>"Email"</Label>
<Input type_="email" placeholder="Enter your email" />
</Text_field>
<Text_field class_="w-full" name="phone" variant=`secondary>
<Label>"Phone"</Label>
<Input type_="tel" placeholder="Enter your phone number" />
</Text_field>
<Text_field class_="w-full" name="company" variant=`secondary>
<Label>"Company"</Label>
<Input placeholder="Enter your company name" />
</Text_field>
<Text_field class_="w-full" name="message" variant=`secondary>
<Label>"Message"</Label>
<Input placeholder="Enter your message" />
</Text_field>
</form>
</Surface>
</Modal.Body>
<Modal.Footer>
<Button slot=`close variant=`secondary>"Cancel"</Button>
<Button slot=`close>"Send Message"</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>
modal_custom_trigger.mlx
module Button = Solidtype_heroui.Button
module Modal = Solidtype_heroui.Modal
let make () =
<Modal>
<Modal.Trigger
class_="group flex items-center gap-3 rounded-2xl bg-surface p-4 \
shadow-xs select-none hover:bg-surface-secondary">
<div
class_="flex size-12 shrink-0 items-center justify-center rounded-xl \
bg-accent-soft text-accent-soft-foreground">
<Gravity.Gear class_="size-6" />
</div>
<div class_="flex flex-1 flex-col gap-0.5">
<p class_="text-sm font-semibold">"Settings"</p>
<p class_="text-xs text-muted">"Manage your preferences"</p>
</div>
</Modal.Trigger>
<Modal.Backdrop>
<Modal.Container>
<Modal.Dialog class_="sm:max-w-[360px]">
<Modal.Close_trigger />
<Modal.Header>
<Modal.Icon class_="bg-accent-soft text-accent-soft-foreground">
<Gravity.Gear class_="size-5" />
</Modal.Icon>
<Modal.Heading>"Settings"</Modal.Heading>
</Modal.Header>
<Modal.Body>
<p>
"Use "
<code>"Modal.Trigger"</code>
" to create custom trigger elements beyond standard buttons. \
This example shows a card-style trigger with icons and \
descriptive text."
</p>
</Modal.Body>
<Modal.Footer>
<Button slot=`close variant=`secondary>"Cancel"</Button>
<Button slot=`close>"Save"</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>
modal_backdrop_variants.mlx
module Jsx = Solidtype.Jsx
module For = Solidtype.For
module Button = Solidtype_heroui.Button
module Modal = Solidtype_heroui.Modal
let variants =
[| (`opaque, "opaque"); (`blur, "blur"); (`transparent, "transparent") |]
let make () =
<div class_="flex flex-wrap gap-4">
<For each=(fun () -> variants)>
(fun (variant, name) _ ->
<Modal>
<Button variant=`secondary>
(fun () -> Jsx.string (String.capitalize_ascii name))
</Button>
<Modal.Backdrop variant=(fun () -> variant)>
<Modal.Container>
<Modal.Dialog class_="sm:max-w-[360px]">
<Modal.Close_trigger />
<Modal.Header>
<Modal.Icon class_="bg-default text-foreground">
<Gravity.Rocket class_="size-5" />
</Modal.Icon>
<Modal.Heading>
"Backdrop: "
(fun () -> Jsx.string (String.capitalize_ascii name))
</Modal.Heading>
</Modal.Header>
<Modal.Body>
<p>
"This modal uses the "
<code>(fun () -> Jsx.string name)</code>
" backdrop variant. Compare the different visual effects: \
opaque provides full opacity, blur adds a backdrop \
filter, and transparent removes the background."
</p>
</Modal.Body>
<Modal.Footer>
<Button class_="w-full" slot=`close>"Continue"</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>)
</For>
</div>
modal_custom_backdrop.mlx
module Button = Solidtype_heroui.Button
module Modal = Solidtype_heroui.Modal
let make () =
<Modal>
<Button variant=`secondary>"Custom Backdrop"</Button>
<Modal.Backdrop
class_="bg-linear-to-t from-black/80 via-black/40 to-transparent \
dark:from-zinc-800/80 dark:via-zinc-800/40"
variant=`blur>
<Modal.Container>
<Modal.Dialog class_="sm:max-w-[360px]">
<Modal.Header class_="items-center text-center">
<Modal.Icon class_="bg-accent-soft text-accent-soft-foreground">
<Gravity.Sparkles class_="size-5" />
</Modal.Icon>
<Modal.Heading>"Premium Backdrop"</Modal.Heading>
</Modal.Header>
<Modal.Body>
<p>
"This backdrop features a sophisticated gradient that \
transitions from a dark color at the bottom to complete \
transparency at the top, combined with a smooth blur effect. \
The gradient automatically adapts its intensity for optimal \
contrast in both light and dark modes."
</p>
</Modal.Body>
<Modal.Footer class_="flex-col-reverse">
<Button class_="w-full" slot=`close>"Amazing!"</Button>
<Button class_="w-full" slot=`close variant=`secondary>
"Close"
</Button>
</Modal.Footer>
<Modal.Close_trigger />
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>

is_dismissable

Controls whether the modal can be dismissed by clicking the overlay backdrop. Defaults to true. Set to false to require explicit close action.

is_keyboard_dismiss_disabled

Controls whether the ESC key can dismiss the modal. When set to true, the ESC key will be disabled and users must use explicit close actions.

modal_dismiss.mlx
module Button = Solidtype_heroui.Button
module Modal = Solidtype_heroui.Modal
let make () =
<div class_="flex max-w-sm flex-col gap-6">
<div class_="flex flex-col gap-2">
<h3 class_="text-lg font-semibold">"is_dismissable"</h3>
<p class_="text-sm text-muted">
"Controls whether the modal can be dismissed by clicking the overlay \
backdrop. Defaults to "
<code>"true"</code>
". Set to "
<code>"false"</code>
" to require explicit close action."
</p>
<Modal>
<Button variant=`secondary>"Open Modal"</Button>
<Modal.Backdrop is_dismissable=false>
<Modal.Container>
<Modal.Dialog class_="sm:max-w-[360px]">
<Modal.Close_trigger />
<Modal.Header>
<Modal.Icon class_="bg-default text-foreground">
<Gravity.Circle_info class_="size-5" />
</Modal.Icon>
<Modal.Heading>"is_dismissable = false"</Modal.Heading>
<p class_="text-sm leading-5 text-muted">
"Clicking the backdrop won't close this modal"
</p>
</Modal.Header>
<Modal.Body>
<p>
"Try clicking outside this modal on the overlay - it won't \
close. You must use the close button or press ESC to \
dismiss it."
</p>
</Modal.Body>
<Modal.Footer>
<Button class_="w-full" slot=`close>"Close"</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>
</div>
<div class_="flex flex-col gap-2">
<h3 class_="text-lg font-semibold">"is_keyboard_dismiss_disabled"</h3>
<p class_="text-sm text-muted">
"Controls whether the ESC key can dismiss the modal. When set to "
<code>"true"</code>
", the ESC key will be disabled and users must use explicit close \
actions."
</p>
<Modal>
<Button variant=`secondary>"Open Modal"</Button>
<Modal.Backdrop is_keyboard_dismiss_disabled=true>
<Modal.Container>
<Modal.Dialog class_="sm:max-w-[360px]">
<Modal.Close_trigger />
<Modal.Header>
<Modal.Icon class_="bg-default text-foreground">
<Gravity.Circle_info class_="size-5" />
</Modal.Icon>
<Modal.Heading>
"is_keyboard_dismiss_disabled = true"
</Modal.Heading>
<p class_="text-sm leading-5 text-muted">
"ESC key is disabled"
</p>
</Modal.Header>
<Modal.Body>
<p>
"Press ESC - nothing happens. You must use the close button \
or click the overlay backdrop to dismiss this modal."
</p>
</Modal.Body>
<Modal.Footer>
<Button class_="w-full" slot=`close>"Close"</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>
</div>
</div>

The second half closes the modal from its own state, where HeroUI’s reads close from the dialog’s render props: the page keeps the state, which the modal follows.

Using slot=`close

The simplest way to close a modal. Add slot=`close to any Button component within the modal. When clicked, it will automatically close the modal.

Using the modal's state

Keep the modal's state in a signal and set it to false. This gives you full control over when and how to close the modal, allowing you to add custom logic before closing.

modal_close_methods.mlx
module S = Solidtype.Solid
module Button = Solidtype_heroui.Button
module Modal = Solidtype_heroui.Modal
(* HeroUI's second dialog closes itself through its render prop's close; here
the page keeps the modal's state, which the buttons set. *)
let make () =
let is_open, set_open = S.create_signal false in
<div class_="flex max-w-2xl flex-col gap-8">
<div class_="flex flex-col gap-2">
<h3 class_="text-lg font-semibold">"Using slot=`close"</h3>
<p class_="text-sm text-muted">
"The simplest way to close a modal. Add "
<code>"slot=`close"</code>
" to any Button component within the modal. When clicked, it will \
automatically close the modal."
</p>
<Modal>
<Button variant=`secondary>"Open Modal"</Button>
<Modal.Backdrop>
<Modal.Container>
<Modal.Dialog class_="sm:max-w-[360px]">
<Modal.Header>
<Modal.Icon class_="bg-accent-soft text-accent-soft-foreground">
<Gravity.Circle_info class_="size-5" />
</Modal.Icon>
<Modal.Heading>"Using slot=`close"</Modal.Heading>
</Modal.Header>
<Modal.Body>
<p>
"Click either button below - both have "
<code>"slot=`close"</code>
" and will close the modal automatically."
</p>
</Modal.Body>
<Modal.Footer>
<Button slot=`close variant=`secondary>"Cancel"</Button>
<Button slot=`close>"Confirm"</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>
</div>
<div class_="flex flex-col gap-2">
<h3 class_="text-lg font-semibold">"Using the modal's state"</h3>
<p class_="text-sm text-muted">
"Keep the modal's state in a "
<code>"signal"</code>
" and set it to "
<code>"false"</code>
". This gives you full control over when and how to close the modal, \
allowing you to add custom logic before closing."
</p>
<Modal is_open=(fun () -> S.get is_open) on_open_change=(S.set set_open)>
<Button variant=`secondary>"Open Modal"</Button>
<Modal.Backdrop>
<Modal.Container>
<Modal.Dialog class_="sm:max-w-[360px]">
<Modal.Header>
<Modal.Icon
class_="bg-success-soft text-success-soft-foreground">
<Gravity.Circle_check class_="size-5" />
</Modal.Icon>
<Modal.Heading>"Using the modal's state"</Modal.Heading>
</Modal.Header>
<Modal.Body>
<p>
"The buttons below set the "
<code>"signal"</code>
" to "
<code>"false"</code>
". You can add validation or other logic before calling "
<code>"S.set set_open false"</code>
"."
</p>
</Modal.Body>
<Modal.Footer>
<Button
variant=`secondary on_click=(fun _ -> S.set set_open false)>
"Cancel"
</Button>
<Button on_click=(fun _ -> S.set set_open false)>
"Confirm"
</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>
</div>
</div>
modal_custom_animations.mlx
module Jsx = Solidtype.Jsx
module For = Solidtype.For
module Button = Solidtype_heroui.Button
module Modal = Solidtype_heroui.Modal
type animation = {
backdrop : string;
container : string;
description : string;
icon : [ `sparkles | `arrow_up_from_line ];
name : string;
}
let animations =
[|
{
backdrop =
String.concat " "
[
"data-[entering]:duration-400";
"data-[entering]:ease-[cubic-bezier(0.16,1,0.3,1)]";
"data-[exiting]:duration-200";
"data-[exiting]:ease-[cubic-bezier(0.7,0,0.84,0)]";
];
container =
String.concat " "
[
"data-[entering]:animate-in";
"data-[entering]:fade-in-0";
"data-[entering]:zoom-in-95";
"data-[entering]:duration-400";
"data-[entering]:ease-[cubic-bezier(0.16,1,0.3,1)]";
"data-[exiting]:animate-out";
"data-[exiting]:fade-out-0";
"data-[exiting]:zoom-out-95";
"data-[exiting]:duration-200";
"data-[exiting]:ease-[cubic-bezier(0.7,0,0.84,0)]";
];
description =
"Physics-based elastic scaling. Simulates a high-damping spring system \
with fast transient response and prolonged settling time. Ideal for \
Modals and Popovers.";
icon = `sparkles;
name = "Kinematic Scale";
};
{
backdrop =
String.concat " "
[
"data-[entering]:duration-500";
"data-[entering]:ease-[cubic-bezier(0.25,1,0.5,1)]";
"data-[exiting]:duration-200";
"data-[exiting]:ease-[cubic-bezier(0.5,0,0.75,0)]";
];
container =
String.concat " "
[
"data-[entering]:animate-in";
"data-[entering]:fade-in-0";
"data-[entering]:slide-in-from-bottom-4";
"data-[entering]:duration-500";
"data-[entering]:ease-[cubic-bezier(0.25,1,0.5,1)]";
"data-[exiting]:animate-out";
"data-[exiting]:fade-out-0";
"data-[exiting]:slide-out-to-bottom-2";
"data-[exiting]:duration-200";
"data-[exiting]:ease-[cubic-bezier(0.5,0,0.75,0)]";
];
description =
"Simulates movement through a medium with fluid resistance. Eliminates \
mechanical linearity for a natural, grounded feel. Perfect for Bottom \
Sheets or Toasts.";
icon = `arrow_up_from_line;
name = "Fluid Slide";
};
|]
let make () =
<div class_="flex flex-wrap gap-4">
<For each=(fun () -> animations)>
(fun (a : animation) _ ->
<Modal>
<Button variant=`secondary>(fun () -> Jsx.string a.name)</Button>
<Modal.Backdrop class_=(fun () -> a.backdrop)>
<Modal.Container class_=(fun () -> a.container)>
<Modal.Dialog class_="sm:max-w-[360px]">
<Modal.Close_trigger />
<Modal.Header>
<Modal.Icon class_="bg-default text-foreground">
(fun () ->
match a.icon with
| `sparkles ->
Jsx.element <Gravity.Sparkles class_="size-5" />
| `arrow_up_from_line ->
Jsx.element
<Gravity.Arrow_up_from_line class_="size-5" />)
</Modal.Icon>
<Modal.Heading>
(fun () -> Jsx.string a.name) " Animation"
</Modal.Heading>
</Modal.Header>
<Modal.Body>
<p class_="mt-1">(fun () -> Jsx.string a.description)</p>
</Modal.Body>
<Modal.Footer>
<Button slot=`close variant=`tertiary>"Close"</Button>
<Button slot=`close>"Try Again"</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>)
</For>
</div>

HeroUI’s Custom Portal example gives the backdrop React Aria’s UNSTABLE_portalContainer, which the port does not take yet: a modal is drawn at the end of the page.

class_ is added after HeroUI’s classes.

modal_tailwind.mlx
module Button = Solidtype_heroui.Button
module Modal = Solidtype_heroui.Modal
let make () =
<Modal>
<Button variant=`secondary>"Open"</Button>
<Modal.Backdrop class_="bg-overlay/50 dark:bg-overlay/65" variant=`blur>
<Modal.Container>
<Modal.Dialog
class_="relative overflow-hidden border border-border/80 \
bg-surface/90 shadow-2xl ring-1 ring-black/5 \
backdrop-blur-xl sm:max-w-[340px] dark:border-border/90 \
dark:bg-surface/85 dark:ring-white/10">
<div
aria_hidden=true
class_="pointer-events-none absolute inset-x-0 top-0 h-20 \
bg-linear-to-b from-neutral-500/8 to-transparent \
dark:from-neutral-400/10"
/>
<div
aria_hidden=true
class_="pointer-events-none absolute inset-x-10 top-0 h-px \
bg-linear-to-r from-transparent via-neutral-400/40 \
to-transparent dark:via-neutral-500/35"
/>
<Modal.Close_trigger />
<Modal.Header class_="relative">
<Modal.Icon
class_="bg-neutral-100 text-neutral-700 dark:bg-neutral-800 \
dark:text-neutral-200">
<Gravity.Circle_check class_="size-5" />
</Modal.Icon>
<Modal.Heading>"Changes saved"</Modal.Heading>
</Modal.Header>
<Modal.Body class_="relative">
<p class_="text-sm text-muted">
"Your draft is synced across devices."
</p>
</Modal.Body>
<Modal.Footer>
<Button class_="w-full" slot=`close>"Done"</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>

HeroUI’s classes are global, so a stylesheet restyles every one, as with HeroUI in React:

@layer components {
.modal__dialog {
@apply rounded-2xl;
}
.modal__heading {
@apply text-lg font-semibold;
}
}
  • .modal__trigger, .modal__backdrop, .modal__container, .modal__dialog, .modal__header, .modal__heading, .modal__icon, .modal__body, .modal__footer, .modal__close-trigger
  • .modal__backdrop--opaque, .modal__backdrop--blur, .modal__backdrop--transparent
  • .modal__container--scroll-outside, .modal__dialog--scroll-inside, .modal__dialog--scroll-outside, .modal__body--scroll-inside, .modal__body--scroll-outside
  • .modal__dialog--xs, --sm, --md, --lg, --cover, --full, and .modal__container--full
  • Entering: [data-entering="true"] on the backdrop and the container
  • Exiting: [data-exiting="true"] on the backdrop and the container
  • Placement: [data-placement="auto"], and the others, on the container and the dialog

Kobalte’s dialog opens, closes, keeps the focus inside it, hides the page outside from a reader and keeps it from scrolling, as React Aria’s modal does. The port draws HeroUI’s backdrop and container around Kobalte’s content, which is the dialog, a <section> as React Aria’s is, and gives it the focus as it opens, where Kobalte gives it to the first button inside, and back to what had it as it closes. A press on a backdrop that stays leaves the focus nowhere, which Kobalte’s trap does not catch: the dialog takes it back, as React Aria’s does. The trigger shows pressed while the modal is open, as HeroUI’s does.

A dismiss button only a reader finds closes a dismissable modal, as React Aria puts one: at the dialog’s start, where React Aria’s is just before it, since Kobalte hides all else from a reader; and a type="button", so a modal inside a form does not submit it. The backdrop and container read --visual-viewport-height from the window’s visual viewport, which a phone’s keyboard shortens, as React Aria gives them.

Each prop beside the name HeroUI’s React component gives it.

React solidtype Type Default Description
isOpen is_open bool Whether it is open, controlled
defaultOpen default_open bool false Whether it starts open
onOpenChange on_open_change bool -> unit Told as it opens or closes
children children elements Its trigger and its backdrop
React solidtype Type Default Description
variant variant `opaque | `blur | `transparent `opaque Its look
isDismissable is_dismissable bool true Whether a press on it closes the modal
isKeyboardDismissDisabled is_keyboard_dismiss_disabled bool false Whether Escape does not
isOpen is_open bool With no Modal around it, whether it is open
onOpenChange on_open_change bool -> unit With no Modal around it, told as it opens or closes
className class_ string Added after HeroUI’s classes
React solidtype Type Default Description
placement placement `auto | `center | `top | `bottom `auto Where the dialog is
scroll scroll `inside | `outside `inside What scrolls a long dialog
size size `xs | `sm | `md | `lg | `cover | `full `md Its width
className class_ string Added after HeroUI’s classes
Section titled “Modal.Dialog, Header, Body, Footer, Icon, Heading”
React solidtype Type Description
className class_ string Added after HeroUI’s classes
children children elements What it holds
React solidtype Type Description
aria-label aria_label string Its name, where its children give none
className class_ string Added after HeroUI’s classes
React solidtype Type Description
className class_ string Added after HeroUI’s classes

Original HeroUI documentation: Modal.

The same HeroUI Modal twice, under the same props: in solidtype on the left, in React on the right. Below them, every attribute of each trigger: a marked row is one they disagree on, and a dimmed row is a framework’s own, which no style reads.

solidtype — OCaml, on Solid and Kobalte
React — TypeScript, on React Aria
attributesolidtypeReact