Dialog
Dialog opens a modal for a task that interrupts the page. The user can dismiss it without making a choice.
- If the user must confirm or cancel a consequential action, use AlertDialog.
- For a panel at the screen edge, such as settings or filters, use Sheet.
- For a mobile-style panel with drag-to-dismiss, use Drawer.
'use client';
import { Button } from '@/components/nxds/components/button/button';
import {
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/nxds/components/dialog/dialog';
export default function DialogDefault() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>
This is a basic dialog with a title and description.
</DialogDescription>
</DialogHeader>
<DialogBody>
<p className="nx:typography-body-default nx:text-foreground">
Dialog content goes here. You can add any content you need.
</p>
</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
This example also needs: Button.
Installation
npx nxds-cli@latest add dialog
Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.
Usage
import {
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/nxds/components/dialog/dialog';
Composition
Dialog
├── DialogTrigger
└── DialogContent
├── DialogHeader
│ ├── DialogTitle
│ └── DialogDescription
├── DialogBody
└── DialogFooter
└── DialogClose
Examples
With description
Add a DialogDescription and a short form to edit a profile.
'use client';
import * as React from 'react';
import { Button } from '@/components/nxds/components/button/button';
import {
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/nxds/components/dialog/dialog';
import { Input } from '@/components/nxds/components/input/input';
import { Label } from '@/components/nxds/components/label/label';
export default function DialogWithDescription() {
const nameId = React.useId();
const usernameId = React.useId();
return (
<Dialog>
<DialogTrigger asChild>
<Button>Edit Profile</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<DialogBody className="nx:grid nx:gap-4 nx:py-4">
<div className="nx:grid nx:grid-cols-4 nx:items-center nx:gap-4">
<Label htmlFor={nameId} className="nx:justify-end">
Name
</Label>
<Input
id={nameId}
defaultValue="John Doe"
className="nx:col-span-3"
/>
</div>
<div className="nx:grid nx:grid-cols-4 nx:items-center nx:gap-4">
<Label htmlFor={usernameId} className="nx:justify-end">
Username
</Label>
<Input
id={usernameId}
defaultValue="@johndoe"
className="nx:col-span-3"
/>
</div>
</DialogBody>
<DialogFooter>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
Scrollable content
Long DialogBody content scrolls while the header and footer stay in place.
'use client';
import { Button } from '@/components/nxds/components/button/button';
import {
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/nxds/components/dialog/dialog';
export default function DialogScrollableContent() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Terms & Conditions</Button>
</DialogTrigger>
<DialogContent className="nx:max-h-[300px]">
<DialogHeader>
<DialogTitle>Terms of Service</DialogTitle>
<DialogDescription>
Please read the following terms carefully.
</DialogDescription>
</DialogHeader>
<DialogBody className="nx:space-y-4 nx:typography-label-default nx:text-foreground">
{Array.from({ length: 10 }).map((_, i) => (
<p key={i}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat.
</p>
))}
</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Decline</Button>
</DialogClose>
<Button>Accept</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
Prop driven content
Pass title, description and body to DialogContent instead of composing the header and body.
'use client';
import { Button } from '@/components/nxds/components/button/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogTrigger,
} from '@/components/nxds/components/dialog/dialog';
export default function DialogPropDrivenContent() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open invite dialog</Button>
</DialogTrigger>
<DialogContent
title="Invite teammate"
description="Send an invitation to a teammate and assign their first workspace role."
body={
<p className="nx:typography-body-default nx:text-foreground">
The invitation expires in seven days and can be revoked from team
settings.
</p>
}
>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Send invite</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
Props
Dialog
Dialog adds no props of its own; it accepts the props of the element or primitive it wraps.
DialogBody
DialogBody adds no props of its own; it accepts the props of the element or primitive it wraps.
DialogClose
DialogClose adds no props of its own; it accepts the props of the element or primitive it wraps.
DialogContent
| Prop | Type | Default | Description |
|---|---|---|---|
body | ReactNode | —No default documented | Content for a generated |
description | ReactNode | —No default documented | Description for a generated header. Ignored when a |
showCloseButton | boolean | true | Shows the close button in the top-right corner. |
title | ReactNode | —No default documented | Title for a generated header. Ignored when |
DialogDescription
DialogDescription adds no props of its own; it accepts the props of the element or primitive it wraps.
DialogFooter
DialogFooter adds no props of its own; it accepts the props of the element or primitive it wraps.
DialogHeader
DialogHeader adds no props of its own; it accepts the props of the element or primitive it wraps.
DialogOverlay
DialogOverlay adds no props of its own; it accepts the props of the element or primitive it wraps.
DialogPortal
DialogPortal adds no props of its own; it accepts the props of the element or primitive it wraps.
DialogTitle
DialogTitle adds no props of its own; it accepts the props of the element or primitive it wraps.
DialogTrigger
DialogTrigger adds no props of its own; it accepts the props of the element or primitive it wraps.
Dialog opens a modal for a task that interrupts the page. The user can dismiss it without making a choice. It renders into document.body, traps focus, and returns focus to the trigger when it closes.
<script setup lang="ts">
import { Button } from '@/components/button';
import {
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/dialog';
</script>
<template>
<Dialog>
<DialogTrigger as-child>
<Button variant="outline">Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription
>This is a basic dialog with a title and
description.</DialogDescription
>
</DialogHeader>
<DialogBody>
<p class="nx:typography-body-default nx:text-foreground">
Dialog content goes here. You can add any content you need.
</p>
</DialogBody>
<DialogFooter>
<DialogClose as-child>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
This example also needs: Button.
Installation
- 1
Install the following dependencies:
npm install @phosphor-icons/vue@^2.2.1 @vueuse/core@^14.1.0 class-variance-authority@^0.7.1 reka-ui@^2.11.0 - 2
Copy and paste the following code into your project.
components/dialog/DialogBody.vue <script setup lang="ts"> import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; import { overlayBodyClassName } from '../overlay-layout/overlay-layout'; export interface DialogBodyProps { class?: HTMLAttributes['class']; } const props = defineProps<DialogBodyProps>(); </script> <template> <div data-slot="dialog-body" tabindex="0" :class="cn(overlayBodyClassName, props.class)" > <slot /> </div> </template>components/dialog/DialogContent.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { DialogClose, DialogContent, type DialogContentEmits, type DialogContentProps as RekaDialogContentProps, DialogPortal, useForwardPropsEmits, } from 'reka-ui'; import { Fragment, type HTMLAttributes, type VNode } from 'vue'; import { IconX } from '../../lib/icons'; import { cn } from '../../lib/utils'; import { overlayCloseButtonClassName, overlayContentVariants, } from '../overlay-layout/overlay-layout'; import DialogBody from './DialogBody.vue'; import DialogDescription from './DialogDescription.vue'; import DialogHeader from './DialogHeader.vue'; import DialogOverlay from './DialogOverlay.vue'; import DialogTitle from './DialogTitle.vue'; export interface DialogContentProps extends RekaDialogContentProps { /** Shows the close button in the top-right corner. */ showCloseButton?: boolean; class?: HTMLAttributes['class']; } defineOptions({ inheritAttrs: false }); const props = withDefaults(defineProps<DialogContentProps>(), { showCloseButton: true, }); const emits = defineEmits<DialogContentEmits>(); const slots = defineSlots<{ default?(): VNode[]; /** * Title for a generated header. Ignored when `DialogHeader` is a direct * child or inside a Fragment. */ title?(): VNode[]; /** * Description for a generated header. Ignored when a `DialogHeader` child is * present. */ description?(): VNode[]; /** * Content for a generated `DialogBody`, rendered before the default slot. * Ignored when a `DialogBody` child is present. */ body?(): VNode[]; }>(); const delegated = reactiveOmit(props, 'class', 'showCloseButton'); const forwarded = useForwardPropsEmits(delegated, emits); function containsComposedSlot( nodes: VNode[] | undefined, part: unknown ): boolean { return (nodes ?? []).some( (node) => node.type === part || (node.type === Fragment && Array.isArray(node.children) && containsComposedSlot(node.children as VNode[], part)) ); } /** Which parts DialogContent generates. The template binds it once per render, so the header and body checks share one read of the default slot. */ function generatedParts() { const children = slots.default?.(); return { header: (slots.title !== undefined || slots.description !== undefined) && !containsComposedSlot(children, DialogHeader), body: slots.body !== undefined && !containsComposedSlot(children, DialogBody), }; } </script> <template> <DialogPortal> <DialogOverlay /> <DialogContent v-bind="{ ...$attrs, ...forwarded }" data-slot="dialog-content" data-variant="default" data-orientation="horizontal" :class="cn(overlayContentVariants(), props.class)" > <template v-for="(generated, index) in [generatedParts()]" :key="index"> <DialogHeader v-if="generated.header"> <DialogTitle v-if="slots.title"><slot name="title" /></DialogTitle> <DialogDescription v-if="slots.description"> <slot name="description" /> </DialogDescription> </DialogHeader> <DialogBody v-if="generated.body"><slot name="body" /></DialogBody> </template> <slot /> <DialogClose v-if="showCloseButton" data-slot="dialog-close-button" :class="overlayCloseButtonClassName" > <IconX class="nx:size-4" /> <span class="nx:sr-only">Close</span> </DialogClose> </DialogContent> </DialogPortal> </template>components/dialog/DialogDescription.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { DialogDescription, type DialogDescriptionProps as RekaDialogDescriptionProps, useForwardProps, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; export interface DialogDescriptionProps extends RekaDialogDescriptionProps { class?: HTMLAttributes['class']; } const props = defineProps<DialogDescriptionProps>(); const delegated = reactiveOmit(props, 'class'); const forwarded = useForwardProps(delegated); </script> <template> <DialogDescription v-bind="forwarded" data-slot="dialog-description" :class=" cn( 'nx:m-0 nx:typography-body-default nx:text-muted-foreground', props.class ) " > <slot /> </DialogDescription> </template>components/dialog/DialogFooter.vue <script setup lang="ts"> import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; export interface DialogFooterProps { class?: HTMLAttributes['class']; } const props = defineProps<DialogFooterProps>(); </script> <template> <div data-slot="dialog-footer" data-orientation="horizontal" :class=" cn( 'nx:flex nx:shrink-0 nx:flex-col-reverse nx:px-6 nx:sm:flex-row nx:sm:justify-end nx:sm:gap-2', props.class ) " > <slot /> </div> </template>components/dialog/DialogHeader.vue <script setup lang="ts"> import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; import { overlayHeaderVariants } from '../overlay-layout/overlay-layout'; export interface DialogHeaderProps { class?: HTMLAttributes['class']; } const props = defineProps<DialogHeaderProps>(); </script> <template> <div data-slot="dialog-header" data-variant="default" :class="cn(overlayHeaderVariants({ variant: 'default' }), props.class)" > <slot /> </div> </template>components/dialog/DialogOverlay.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { DialogOverlay, type DialogOverlayProps as RekaDialogOverlayProps, useForwardProps, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; import { overlayScrimVariants } from '../overlay-layout/overlay-layout'; export interface DialogOverlayProps extends RekaDialogOverlayProps { class?: HTMLAttributes['class']; } const props = defineProps<DialogOverlayProps>(); const delegated = reactiveOmit(props, 'class'); const forwarded = useForwardProps(delegated); </script> <template> <DialogOverlay v-bind="forwarded" data-slot="dialog-overlay" :class="cn(overlayScrimVariants(), props.class)" /> </template>components/dialog/DialogTitle.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { DialogTitle, type DialogTitleProps as RekaDialogTitleProps, useForwardProps, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; export interface DialogTitleProps extends RekaDialogTitleProps { class?: HTMLAttributes['class']; } const props = defineProps<DialogTitleProps>(); const delegated = reactiveOmit(props, 'class'); const forwarded = useForwardProps(delegated); </script> <template> <DialogTitle v-bind="forwarded" data-slot="dialog-title" :class="cn('nx:m-0 nx:typography-heading-xsmall', props.class)" > <slot /> </DialogTitle> </template>components/dialog/index.ts export { default as DialogBody, type DialogBodyProps } from './DialogBody.vue'; export { default as DialogContent, type DialogContentProps, } from './DialogContent.vue'; export { default as DialogDescription, type DialogDescriptionProps, } from './DialogDescription.vue'; export { default as DialogFooter, type DialogFooterProps, } from './DialogFooter.vue'; export { default as DialogHeader, type DialogHeaderProps, } from './DialogHeader.vue'; export { default as DialogOverlay, type DialogOverlayProps, } from './DialogOverlay.vue'; export { default as DialogTitle, type DialogTitleProps, } from './DialogTitle.vue'; export { DialogRoot as Dialog, DialogClose, type DialogCloseProps, DialogPortal, type DialogPortalProps, type DialogRootProps as DialogProps, DialogTrigger, type DialogTriggerProps, } from 'reka-ui';components/overlay-layout/overlay-layout.ts import { cva, type VariantProps } from 'class-variance-authority'; /** * Popover-family surfaces share the translucent/blurred recipe; Tooltip uses a * solid carve-out because its small, brief text appears over arbitrary content. */ export const popoverSurfaceClassName = [ 'nx:rounded-md', 'nx:border-default', 'nx:border-border-default', 'nx:bg-popover-alpha', 'nx:backdrop-blur-md', 'nx:text-popover-foreground', 'nx:typography-body-default', 'nx:shadow-lg', ].join(' '); export const tooltipSurfaceClassName = [ 'nx:rounded-md', 'nx:border-default', 'nx:border-border-default', 'nx:bg-popover', 'nx:text-popover-foreground', 'nx:typography-body-default', 'nx:shadow-lg', ].join(' '); export const navigationMenuInlinePopoverSurfaceClassName = [ 'nx:group-data-[viewport=false]/navigation-menu:rounded-md', 'nx:group-data-[viewport=false]/navigation-menu:border-default', 'nx:group-data-[viewport=false]/navigation-menu:border-border-default', 'nx:group-data-[viewport=false]/navigation-menu:bg-popover-alpha', 'nx:group-data-[viewport=false]/navigation-menu:backdrop-blur-md', 'nx:group-data-[viewport=false]/navigation-menu:text-popover-foreground', 'nx:group-data-[viewport=false]/navigation-menu:shadow-lg', ].join(' '); export const navigationMenuInlinePopoverTransitionClassName = [ 'nx:group-data-[viewport=false]/navigation-menu:transition-[opacity,scale]', 'nx:group-data-[viewport=false]/navigation-menu:duration-fast', 'nx:group-data-[viewport=false]/navigation-menu:ease-move', 'nx:group-data-[viewport=false]/navigation-menu:data-[state=closed]:scale-95', 'nx:group-data-[viewport=false]/navigation-menu:data-[state=closed]:opacity-0', 'nx:group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-overlay-presence-exit', ].join(' '); // `animate-overlay-presence-exit` is a non-visual "presence bridge": Reka's // Presence only waits on `animationName`, not `transitionend`, so this inert // animation keeps a closing overlay mounted while the opacity/scale/translate // TRANSITIONS below run the visible exit. It animates an unread custom property (see // generateMotionUtilitiesCSS in @nxds/core), never a transitioned property, so it // cannot override those transitions. const overlayPresenceExitClassName = 'nx:data-[state=closed]:animate-overlay-presence-exit'; export const overlayFloatingTransitionClassName = [ 'nx:transition-[opacity,scale]', 'nx:duration-fast', 'nx:ease-move', 'nx:data-[state=closed]:scale-95', 'nx:data-[state=closed]:opacity-0', overlayPresenceExitClassName, ].join(' '); const overlayContentTransitionClassName = [ 'nx:transition-[opacity,scale]', 'nx:duration-default', 'nx:ease-move', 'nx:data-[state=closed]:scale-95', 'nx:data-[state=closed]:opacity-0', overlayPresenceExitClassName, ].join(' '); export const overlayScrimTransitionClassName = [ 'nx:transition-opacity', 'nx:duration-default', 'nx:ease-move', 'nx:data-[state=closed]:opacity-0', overlayPresenceExitClassName, ].join(' '); export const overlayPanelTransitionClassName = [ 'nx:transition-[translate]', 'nx:duration-slow', 'nx:ease-move', overlayPresenceExitClassName, ].join(' '); export const overlayContentVariants = cva( [ // Centred with margins, not a translate: a transform would make this the // containing block for fixed-position popups a dialog holds. 'nx:fixed nx:inset-0 nx:z-modal nx:m-auto nx:flex nx:h-fit nx:w-full nx:max-w-lg nx:flex-col', 'nx:max-h-[calc(100svh-2rem)] nx:overflow-hidden', 'nx:gap-4 nx:border-default nx:border-border-default nx:bg-container nx:py-6 nx:shadow-lg', overlayContentTransitionClassName, 'nx:sm:rounded-lg', ].join(' ') ); export const overlayScrimVariants = cva( [ 'nx:fixed nx:inset-0 nx:z-modal nx:bg-overlay', overlayScrimTransitionClassName, ].join(' ') ); export const overlayHeaderVariants = cva( 'nx:flex nx:shrink-0 nx:flex-col nx:gap-1 nx:px-6', { variants: { variant: { default: 'nx:text-center nx:sm:text-left', center: 'nx:items-center nx:text-center', }, }, defaultVariants: { variant: 'default', }, } ); export const overlayBodyClassName = [ 'nx:min-h-0', 'nx:overflow-y-auto', 'nx:px-6', 'nx:focus-visible:outline-2', 'nx:focus-visible:outline-focus-default', 'nx:focus-visible:[outline-offset:-2px]', ].join(' '); export const overlayFooterVariants = cva( 'nx:flex nx:shrink-0 nx:gap-2 nx:px-6', { variants: { orientation: { horizontal: 'nx:flex-col nx:sm:flex-row nx:sm:items-center nx:sm:justify-end', vertical: 'nx:flex-col nx:items-stretch nx:*:w-full', }, }, defaultVariants: { orientation: 'horizontal', }, } ); export const overlayCloseButtonClassName = [ 'nx:absolute nx:right-6 nx:top-6 nx:rounded-sm nx:border-0 nx:bg-transparent nx:p-1 nx:text-muted-foreground-subtle', 'nx:transition-control', 'nx:hover:bg-container-hover nx:hover:text-foreground', 'nx:focus-visible:bg-container-hover nx:focus-visible:text-foreground', 'nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default', 'nx:disabled:pointer-events-none', ].join(' '); export type OverlayHeaderVariants = VariantProps<typeof overlayHeaderVariants>; export type OverlayVariant = NonNullable< VariantProps<typeof overlayHeaderVariants>['variant'] >; export type OverlayButtonOrientation = NonNullable< VariantProps<typeof overlayFooterVariants>['orientation'] >; export type OverlayLayoutContextValue = { variant: OverlayVariant; buttonOrientation: OverlayButtonOrientation; }; export const defaultOverlayLayout: OverlayLayoutContextValue = { variant: 'default', buttonOrientation: 'horizontal', }; export function resolveOverlayButtonOrientation( variant: OverlayVariant ): OverlayButtonOrientation { return variant === 'center' ? 'vertical' : 'horizontal'; }lib/icons.ts /** * Internal Icons * * This barrel file re-exports icons used internally by the design system. * These are hardcoded icons for component states (loading, validation, navigation). * * User-facing icons should be passed via props by consumers. * * This set draws Phosphor. `nxds init --icons` installs the same names drawn * by another library. */ import { PhCircle } from '@phosphor-icons/vue'; import { type FunctionalComponent, h } from 'vue'; // Loading states export { PhCircleNotch as IconLoader2 } from '@phosphor-icons/vue'; // Navigation export { PhArrowDown as IconArrowDown, PhArrowLeft as IconArrowLeft, PhArrowRight as IconArrowRight, PhArrowUp as IconArrowUp, PhCaretDown as IconChevronDown, PhCaretLeft as IconChevronLeft, PhCaretRight as IconChevronRight, PhCaretUp as IconChevronUp, PhDotsThree as IconDots, PhSidebarSimple as IconLayoutSidebar, PhCaretUpDown as IconSelector, } from '@phosphor-icons/vue'; // Actions export { PhCheck as IconCheck, PhCopy as IconCopy, PhMinus as IconMinus, PhPencilSimple as IconPencil, PhPlus as IconPlus, PhX as IconX, } from '@phosphor-icons/vue'; export const IconCircleFilled: FunctionalComponent = (_props, { attrs }) => h(PhCircle, { weight: 'fill', ...attrs }); // Filter field types export { PhCalendarBlank as IconCalendar, PhHash as IconHash, PhTextAa as IconLetterCase, PhListBullets as IconList, } from '@phosphor-icons/vue'; // Search / input export { PhMagnifyingGlass as IconSearch } from '@phosphor-icons/vue'; // Alerts/Status export { PhWarningCircle as IconAlertCircle, PhWarning as IconAlertTriangle, PhCheckCircle as IconCircleCheck, PhInfo as IconInfoCircle, } from '@phosphor-icons/vue'; // Layout export { PhDotsSixVertical as IconGripVertical, PhPalette as IconPalette, } from '@phosphor-icons/vue'; - 3
Keep each file at the path shown above it.
Paths are relative to
packages/vue/src/. Keep the same folders under your@/root, and the imports between files work without edits.
Usage
import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/dialog';
Examples
With description
Add a DialogDescription and a short form to edit a profile.
<script setup lang="ts">
import { useId } from 'vue';
import { Button } from '@/components/button';
import {
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/dialog';
const nameId = useId();
const usernameId = useId();
</script>
<template>
<Dialog>
<DialogTrigger as-child>
<Button>Edit Profile</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription
>Make changes to your profile here. Click save when you're
done.</DialogDescription
>
</DialogHeader>
<DialogBody class="nx:grid nx:gap-4 nx:py-4">
<div class="nx:grid nx:grid-cols-4 nx:items-center nx:gap-4">
<label :for="nameId" class="nx:justify-end">Name</label>
<input :id="nameId" value="John Doe" class="nx:col-span-3" />
</div>
<div class="nx:grid nx:grid-cols-4 nx:items-center nx:gap-4">
<label :for="usernameId" class="nx:justify-end">Username</label>
<input :id="usernameId" value="@johndoe" class="nx:col-span-3" />
</div>
</DialogBody>
<DialogFooter>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
Scrollable content
Long DialogBody content scrolls while the header and footer stay in place.
<script setup lang="ts">
import { Button } from '@/components/button';
import {
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/dialog';
</script>
<template>
<Dialog>
<DialogTrigger as-child>
<Button variant="outline">Terms & Conditions</Button>
</DialogTrigger>
<DialogContent class="nx:max-h-[300px]">
<DialogHeader>
<DialogTitle>Terms of Service</DialogTitle>
<DialogDescription
>Please read the following terms carefully.</DialogDescription
>
</DialogHeader>
<DialogBody
class="nx:space-y-4 nx:typography-label-default nx:text-foreground"
>
<p v-for="i in 10" :key="i">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat.
</p>
</DialogBody>
<DialogFooter>
<DialogClose as-child>
<Button variant="outline">Decline</Button>
</DialogClose>
<Button>Accept</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
Prop driven content
Fill the #title, #description and #body slots of DialogContent instead of composing the header and body.
<script setup lang="ts">
import { Button } from '@/components/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogTrigger,
} from '@/components/dialog';
</script>
<template>
<Dialog>
<DialogTrigger as-child>
<Button variant="outline">Open invite dialog</Button>
</DialogTrigger>
<DialogContent>
<template #title>Invite teammate</template>
<template #description
>Send an invitation to a teammate and assign their first workspace
role.</template
>
<template #body>
<p class="nx:typography-body-default nx:text-foreground">
The invitation expires in seven days and can be revoked from team
settings.
</p>
</template>
<DialogFooter>
<DialogClose as-child>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Send invite</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
Props
DialogBody
DialogBody adds no props of its own; it accepts the props of the element or primitive it wraps.
DialogContent
| Prop | Type | Default | Description |
|---|---|---|---|
showCloseButton | boolean | true | Shows the close button in the top-right corner. |
DialogDescription
DialogDescription adds no props of its own; it accepts the props of the element or primitive it wraps.
DialogFooter
DialogFooter adds no props of its own; it accepts the props of the element or primitive it wraps.
DialogHeader
DialogHeader adds no props of its own; it accepts the props of the element or primitive it wraps.
DialogOverlay
DialogOverlay adds no props of its own; it accepts the props of the element or primitive it wraps.
DialogTitle
DialogTitle adds no props of its own; it accepts the props of the element or primitive it wraps.