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 right-3 top-2',
)}
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 w-full bg-background p-6 text-foreground transition-all backdrop:brightness-50 backdrop:backdrop-blur-sm',
'data-[closing]:duration-300 data-[open]:duration-300 data-[open]:animate-in data-[closing]:animate-out',
'backdrop:data-[closing]:duration-300 backdrop:data-[open]:duration-300 backdrop:data-[open]:animate-in backdrop:data-[closing]:animate-out backdrop:data-[closing]:fade-out backdrop:data-[open]:fade-in',
],
{
variants: {
position: {
center:
'max-w-lg rounded-base shadow-lg data-[state=closed]:fade-out data-[state=open]:fade-in data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=open]:slide-in-from-bottom-2 backdrop:data-[closing]:fade-out backdrop:data-[open]:fade-in',
top: 'inset-x-0 top-0 mt-0 rounded-b-base border-b data-[closing]:slide-out-to-top data-[open]:slide-in-from-top',
bottom:
'inset-x-0 bottom-0 mb-0 rounded-t-base border-t data-[closing]:slide-out-to-bottom data-[open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 ml-0 h-full max-w-sm rounded-r-base border-r data-[closing]:slide-out-to-left data-[open]:slide-in-from-left',
right:
'inset-y-0 right-0 mr-0 h-full max-w-sm rounded-l-base 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 right-3 top-2',
)}
type="submit"
>
<LuX class="h-5 w-5" />
</Modal.Close>
</Modal.Panel>
</Modal.Root>
);
});