Modal
HeroUIA 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.
module Button = Solidtype_heroui.Buttonmodule 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>Anatomy
Section titled “Anatomy”<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>Examples
Section titled “Examples”module Jsx = Solidtype.Jsxmodule For = Solidtype.Formodule Button = Solidtype_heroui.Buttonmodule 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>Placement
Section titled “Placement”module Jsx = Solidtype.Jsxmodule For = Solidtype.Formodule Button = Solidtype_heroui.Buttonmodule 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>Scroll Behavior
Section titled “Scroll Behavior”module S = Solidtype.Solidmodule Jsx = Solidtype.Jsxmodule For = Solidtype.Formodule Button = Solidtype_heroui.Buttonmodule Modal = Solidtype_heroui.Modalmodule Radio = Solidtype_heroui.Radiomodule 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>Controlled State
Section titled “Controlled State”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
module S = Solidtype.Solidmodule Jsx = Solidtype.Jsxmodule Button = Solidtype_heroui.Buttonmodule 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>With Form
Section titled “With Form”module Button = Solidtype_heroui.Buttonmodule Input = Solidtype_heroui.Inputmodule Label = Solidtype_heroui.Labelmodule Modal = Solidtype_heroui.Modalmodule Surface = Solidtype_heroui.Surfacemodule 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>Custom Trigger
Section titled “Custom Trigger”Settings
Manage your preferences
module Button = Solidtype_heroui.Buttonmodule 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>Backdrop Variants
Section titled “Backdrop Variants”module Jsx = Solidtype.Jsxmodule For = Solidtype.Formodule Button = Solidtype_heroui.Buttonmodule 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>Custom Backdrop
Section titled “Custom Backdrop”module Button = Solidtype_heroui.Buttonmodule 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>Dismiss Behavior
Section titled “Dismiss Behavior”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.
module Button = Solidtype_heroui.Buttonmodule 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>Close Methods
Section titled “Close Methods”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.
module S = Solidtype.Solidmodule Button = Solidtype_heroui.Buttonmodule 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>Custom Animations
Section titled “Custom Animations”module Jsx = Solidtype.Jsxmodule For = Solidtype.Formodule Button = Solidtype_heroui.Buttonmodule 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>Custom Portal
Section titled “Custom Portal”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.
Customization
Section titled “Customization”Tailwind CSS
Section titled “Tailwind CSS”class_ is added after HeroUI’s classes.
module Button = Solidtype_heroui.Buttonmodule 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>Global CSS
Section titled “Global CSS”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; }}Styling Reference
Section titled “Styling Reference”CSS Classes
Section titled “CSS Classes”.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
Interactive States
Section titled “Interactive States”- 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
How it is made
Section titled “How it is made”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.
API Reference
Section titled “API Reference”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 |
Modal.Backdrop
Section titled “Modal.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 |
Modal.Container
Section titled “Modal.Container”| 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 |
Modal.Dialog, Header, Body, Footer, Icon, Heading
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 |
Modal.Trigger
Section titled “Modal.Trigger”| React | solidtype | Type | Description |
|---|---|---|---|
aria-label |
aria_label |
string |
Its name, where its children give none |
className |
class_ |
string |
Added after HeroUI’s classes |
Modal.CloseTrigger
Section titled “Modal.CloseTrigger”| React | solidtype | Type | Description |
|---|---|---|---|
className |
class_ |
string |
Added after HeroUI’s classes |
Beside React
Section titled “Beside React”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.
| attribute | solidtype | React |
|---|
