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 { 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';
import { type SubmitHandler } from 'react-hook-form';

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 { 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';
import { BuildingIcon, UserIcon } from 'lucide-react';
import { type SubmitHandler } from 'react-hook-form';

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' });