Modal
A window overlaid on either the primary window or another modal window, rendering the content underneath inert.
import { component$, useSignal } from '@builder.io/qwik';
import { cn } from '@qwik-ui/utils';
import { LuX } from '@qwikest/icons/lucide';
import { Button, Input, Label, Modal, buttonVariants } from '~/components/ui';
export default component$(() => {
const show = useSignal(false);
return (
<Modal.Root bind:show={show}>
<Modal.Trigger class={[buttonVariants({ look: 'outline' })]}>
Open modal
</Modal.Trigger>
<Modal.Panel>
<Modal.Title>Edit Profile</Modal.Title>
<Modal.Description>
Make changes to your profile here. Click save when you're done.
</Modal.Description>
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label for="name" class="text-right">
Name
</Label>
<Input name="name" id="name" defaultValue="Pedro Duarte" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="username" class="text-right">
Username
</Label>
<Input
name="username"
id="username"
defaultValue="@peduarte"
class="col-span-3"
/>
</div>
</div>
<footer>
<Button look="primary" onClick$={() => (show.value = false)}>
Save
</Button>
</footer>
<Modal.Close
class={cn(
buttonVariants({ size: 'icon', look: 'link' }),
'absolute top-2 right-3',
)}
type="submit"
>
<LuX class="h-5 w-5" />
</Modal.Close>
</Modal.Panel>
</Modal.Root>
);
});
Installation
Run the following cli command or copy/paste the component code into your project
qwik-ui add modal
import { type PropsOf, Slot, component$ } from '@builder.io/qwik';
import { Modal as HeadlessModal } from '@qwik-ui/headless';
import { cn } from '@qwik-ui/utils';
import { cva, type VariantProps } from 'class-variance-authority';
const Root = HeadlessModal.Root;
const Trigger = HeadlessModal.Trigger;
const Close = HeadlessModal.Close;
export const panelVariants = cva(
[
'fixed bg-background p-6 text-foreground transition-all backdrop:brightness-50 backdrop:backdrop-blur-xs',
'data-closing:animate-out data-closing:duration-300 data-open:animate-in data-open:duration-300',
'data-closing:backdrop:animate-out data-closing:backdrop:duration-300 data-closing:backdrop:fade-out data-open:backdrop:animate-in data-open:backdrop:duration-300 data-open:backdrop:fade-in',
],
{
variants: {
position: {
center:
'rounded-base m-auto max-w-lg shadow-lg data-closed:zoom-out-95 data-closed:fade-out data-closing:backdrop:fade-out data-open:zoom-in-95 data-open:fade-in data-open:slide-in-from-bottom-2 data-open:backdrop:fade-in',
top: 'rounded-b-base inset-x-0 top-0 mb-auto w-full border-b data-closing:slide-out-to-top data-open:slide-in-from-top',
bottom:
'rounded-t-base inset-x-0 bottom-0 mt-auto w-full border-t data-closing:slide-out-to-bottom data-open:slide-in-from-bottom',
left: 'rounded-r-base inset-y-0 left-0 mr-auto h-full max-w-sm border-r data-closing:slide-out-to-left data-open:slide-in-from-left',
right:
'rounded-l-base inset-y-0 right-0 ml-auto h-full max-w-sm border-l data-closing:slide-out-to-right data-open:slide-in-from-right',
},
},
defaultVariants: {
position: 'center',
},
},
);
type PanelProps = PropsOf<typeof HeadlessModal.Panel> &
VariantProps<typeof panelVariants>;
const Panel = component$<PanelProps>(({ position, ...props }) => {
return (
<HeadlessModal.Panel {...props} class={cn(panelVariants({ position }), props.class)}>
<Slot />
</HeadlessModal.Panel>
);
});
const Title = component$<PropsOf<'h2'>>(({ ...props }) => {
return (
<HeadlessModal.Title
{...props}
class={cn('text-lg font-semibold tracking-tight', props.class)}
>
<Slot />
</HeadlessModal.Title>
);
});
const Description = component$<PropsOf<'p'>>(({ ...props }) => {
return (
<HeadlessModal.Description
{...props}
class={cn('text-sm text-muted-foreground', props.class)}
>
<Slot />
</HeadlessModal.Description>
);
});
export const Modal = {
Root,
Trigger,
Close,
Panel,
Title,
Description,
};
Usage
import { Button, Modal } from '~/components/ui';
<Modal.Root bind:show={show}>
<Modal.Trigger>Open modal</Modal.Trigger>
<Modal.Panel>
<Modal.Title>Title</Modal.Title>
<Modal.Description>Description</Modal.Description>
<div>...</div>
<footer>
<Button look="primary" onClick$={() => (show.value = false)}>
Save
</Button>
</footer>
<Modal.Close>
<LuX class="h-5 w-5" />
</Modal.Close>
</Modal.Panel>
</Modal.Root>
Examples
import { PropsOf, Slot, component$, useSignal } from '@builder.io/qwik';
import { cn } from '@qwik-ui/utils';
import { LuX } from '@qwikest/icons/lucide';
import { Button, Input, Label, Modal, buttonVariants } from '~/components/ui';
export default component$(() => {
return (
<>
<Sheet position="top">Top</Sheet>
<div class="my-4 flex space-x-24">
<Sheet position="left">Left</Sheet>
<Sheet position="right">Right</Sheet>
</div>
<Sheet position="bottom">Bottom</Sheet>
</>
);
});
export const Sheet = component$<PropsOf<typeof Modal.Panel>>(({ ...props }) => {
const show = useSignal(false);
return (
<Modal.Root bind:show={show}>
<Modal.Trigger class={[buttonVariants({ look: 'ghost' }), 'w-20']}>
<Slot />
</Modal.Trigger>
<Modal.Panel {...props}>
<Modal.Title>Edit Profile</Modal.Title>
<Modal.Description>
Make changes to your profile here. Click save when you're done.
</Modal.Description>
<div class="mt-6">
<Label for="name" class="text-right">
Name
</Label>
<Input
name="name"
id="name"
defaultValue="Pedro Duarte"
class="col-span-3 w-[300px]"
/>
</div>
<footer class="mt-6">
<Button look="primary" onClick$={() => (show.value = false)}>
Save
</Button>
</footer>
<Modal.Close
class={cn(
buttonVariants({ size: 'icon', look: 'link' }),
'absolute top-2 right-3',
)}
type="submit"
>
<LuX class="h-5 w-5" />
</Modal.Close>
</Modal.Panel>
</Modal.Root>
);
});