Demo
Apps
Dashboard

Modals

Learn how to use modals in the starter kit.

The starter kit uses NiceModal as a modal manager. This includes dialogs, sheets, drawers and so on.

1. Create modal component

Here, we will create the AddItemModal component using NiceModal to manage modal visibility, react-hook-form for form handling, and Shadcn UI for the UI elements.

apps/dashboard/components/items/add-item-modal.tsx
'use client';import NiceModal from '@ebay/nice-modal-react';import { Button } from '@workspace/ui/components/button';import { FormProvider } from '@workspace/ui/components/form';import { cn } from '@workspace/ui/lib/utils';import { type SubmitHandler } from 'react-hook-form';import { addItem } from '~/actions/items/add-item';import { useEnhancedModal } from '~/hooks/use-enhanced-modal';import { useZodForm } from '~/hooks/use-zod-form';import {  addItemSchema,  type AddItemSchema} from '~/schemas/items/add-item-schema';export const AddItemModal = NiceModal.create(() => {  const modal = useEnhancedModal();  const methods = useZodForm({    schema: addItemSchema,    mode: 'onSubmit',    defaultValues: {      name: ''    }  });  const title = 'Add Item';  const description = 'Create a new item by filling out the form below.';  const canSubmit =    !methods.formState.isSubmitting &&    (!methods.formState.isSubmitted || methods.formState.isDirty);  const onSubmit: SubmitHandler<AddItemSchema> = async (values) => {    // handle submission  };  return (    <FormProvider {...methods}>      <Dialog open={modal.visible}>        <DialogContent          className="max-w-sm"          onClose={modal.handleClose}          onAnimationEndCapture={modal.handleAnimationEndCapture}        >          <DialogHeader>            <DialogTitle>{title}</DialogTitle>            <DialogDescription className="sr-only">              {description}            </DialogDescription>          </DialogHeader>          <form            className="space-y-4"            onSubmit={methods.handleSubmit(onSubmit)}          >            {/* Implementation */}          </form>          <DialogFooter>            <Button              type="button"              variant="outline"              onClick={modal.handleClose}            >              Cancel            </Button>            <Button              type="button"              variant="default"              disabled={!canSubmit}              loading={methods.formState.isSubmitting}              onClick={methods.handleSubmit(onSubmit)}            >              Save            </Button>          </DialogFooter>        </DialogContent>      </Dialog>    </FormProvider>  );});

2. Make the modal responsive

A common tactic is to render a mobile drawer (also sometimes called a bottom sheet) instead of a dialog for modals.

Putting it together:

apps/dashboard/components/items/add-item-modal.tsx
'use client';import NiceModal, { type NiceModalHocProps } from '@ebay/nice-modal-react';import { ItemRecord } from '@workspace/database';import { Button } from '@workspace/ui/components/button';import {  Dialog,  DialogContent,  DialogDescription,  DialogFooter,  DialogHeader,  DialogTitle} from '@workspace/ui/components/dialog';import {  Drawer,  DrawerContent,  DrawerDescription,  DrawerFooter,  DrawerHeader,  DrawerTitle} from '@workspace/ui/components/drawer';import { FormProvider } from '@workspace/ui/components/form';import { Input } from '@workspace/ui/components/input';import { useMediaQuery } from '@workspace/ui/hooks/use-media-query';import { MediaQueries } from '@workspace/ui/lib/media-queries';import { cn } from '@workspace/ui/lib/utils';import { BuildingIcon, UserIcon } from 'lucide-react';import { type SubmitHandler } from 'react-hook-form';import { addItem } from '~/actions/items/add-item';import { useEnhancedModal } from '~/hooks/use-enhanced-modal';import { useZodForm } from '~/hooks/use-zod-form';import { itemRecordLabel } from '~/lib/labels';import {  addItemSchema,  type AddItemSchema} from '~/schemas/items/add-item-schema';export const AddItemModal = NiceModal.create(() => {  const modal = useEnhancedModal();  const mdUp = useMediaQuery(MediaQueries.MdUp, { ssr: false });  const methods = useZodForm({    schema: addItemSchema,    mode: 'onSubmit',    defaultValues: {      record: ItemRecord.PERSON,      name: '',      description: '',      price: ''    }  });  const title = 'Add Item';  const description = 'Create a new item by filling out the form below.';  const canSubmit =    !methods.formState.isSubmitting &&    (!methods.formState.isSubmitted || methods.formState.isDirty);  const onSubmit: SubmitHandler<AddItemSchema> = async (values) => {    // handle submission  };  const renderForm = (    <form      className={cn('space-y-4', !mdUp && 'p-4')}      onSubmit={methods.handleSubmit(onSubmit)}    >      {/* Implementation */}    </form>  );  const renderButtons = (    <>      <Button        type="button"        variant="outline"        onClick={modal.handleClose}      >        Cancel      </Button>      <Button        type="button"        variant="default"        disabled={!canSubmit}        loading={methods.formState.isSubmitting}        onClick={methods.handleSubmit(onSubmit)}      >        Save      </Button>    </>  );  return (    <FormProvider {...methods}>      {mdUp ? (        <Dialog open={modal.visible}>          <DialogContent            className="max-w-sm"            onClose={modal.handleClose}            onAnimationEndCapture={modal.handleAnimationEndCapture}          >            <DialogHeader>              <DialogTitle>{title}</DialogTitle>              <DialogDescription className="sr-only">                {description}              </DialogDescription>            </DialogHeader>            {renderForm}            <DialogFooter>{renderButtons}</DialogFooter>          </DialogContent>        </Dialog>      ) : (        <Drawer          open={modal.visible}          onOpenChange={modal.handleOpenChange}        >          <DrawerContent>            <DrawerHeader className="text-left">              <DrawerTitle>{title}</DrawerTitle>              <DrawerDescription className="sr-only">                {description}              </DrawerDescription>            </DrawerHeader>            {renderForm}            <DrawerFooter className="flex-col-reverse pt-4">              {renderButtons}            </DrawerFooter>          </DrawerContent>        </Drawer>      )}    </FormProvider>  );});

3. Calling the modal from client component

This is quite easy, we just can pass the modal component as argument:

apps/dashboard/components/items/my-other-component.tsx
const handleShowAddItemModal = (): void => {  NiceModal.show(AddItemModal);};

4. Adding modal props

The NiceModal.create function takes a generic so we can define expected props.

apps/dashboard/components/items/add-item-modal.tsx
export type AddItemModalProps = NiceModalHocProps & {  name: string;};export const AddItemModal = NiceModal.create<AddItemModalProps>(({ name }) => {  // ...});

And calling it from the client component:

apps/dashboard/components/items/my-other-component.tsx
NiceModal.show<AddItemModalProps>(AddItemModal, { name: 'John Doe' });