npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dynamic-modal

v1.1.26

Published

The dynamic-modal is a solution of creation different modals into project using a json configuration file

Downloads

187

Readme

dynamic-modal

dynamic-modal is a React library for building configurable modals from JSON. Instead of hand-writing a modal UI every time, you describe fields, actions, and conditional behavior in a config object and open it through a hook.

It is designed for projects that want:

  • reusable modal definitions
  • dynamic forms inside modals
  • conditional rendering with renderIf
  • conditional enabling with enableIf
  • dependent remote options with liveData
  • full UI customization through your own design system components

Compatibility

According to package.json, this library is compatible with:

  • react: ^18.0.0 || ^19.0.0
  • react-dom: ^18.0.0 || ^19.0.0
  • react-hook-form: ^7.54.2

The library itself is currently built with:

  • react: ^19.0.0
  • react-dom: ^19.0.0
  • react-hook-form: ^7.54.2

It works well in React apps and in Next.js projects that support client components.

Installation

npm install dynamic-modal

If your project does not already include the required peers, install them too:

npm install react react-dom react-hook-form

Exports

The package exposes:

  • DynamicModal
  • useModalHandler
  • ComponentState
  • ComponentStateContext
  • IComponentState
  • IModalConfigLoader
  • IModalConfigProps
  • IModalRenderCondition
  • IModalField
  • IModalLiveDataCondition
  • IOption

Mental model

You use the library in 4 steps:

  1. Define the UI components the modal should use in your app.
  2. Wrap your app with ComponentState.
  3. Render DynamicModal and control it with useModalHandler.
  4. Build modal configs as plain objects and open them when needed.

1. Provide your own components

dynamic-modal does not force a UI kit on you. You provide your own inputs, selects, buttons, toggles, and textarea components through ComponentState, so the modal matches your app visually.

Example:

'use client';

import { ReactNode } from 'react';
import {
  Autocomplete,
  AutocompleteItem,
  Button,
  Input,
  Select,
  SelectItem,
  Switch,
  Textarea,
} from '@heroui/react';
import type { IComponentState } from 'dynamic-modal';

export const modalComponents: IComponentState = {
  ModalButtonCancel: ({ text, color, ...props }) => (
    <Button {...props} color={color as any} variant="bordered">
      {text}
    </Button>
  ),
  ModalButtonAction: ({ text, color, ...props }) => (
    <Button {...props} color={color as any} variant="solid">
      {text}
    </Button>
  ),
  Button: ({ text, color, variant, ...props }) => (
    <Button {...props} color={color as any} variant={variant as any}>
      {text}
    </Button>
  ),
  Input: ({ invalid, error, disabled, onChange, value, ...props }) => (
    <Input
      {...props}
      value={value ?? ''}
      onValueChange={onChange}
      errorMessage={error?.message}
      isInvalid={invalid}
      isDisabled={disabled}
    />
  ),
  Select: ({
    options,
    invalid,
    error,
    isMulti,
    isSearch,
    disabled,
    onChange,
    value,
    ...props
  }) =>
    isSearch ? (
      <Autocomplete
        {...props}
        selectedKey={value}
        onSelectionChange={onChange as any}
        errorMessage={error?.message}
        isInvalid={invalid}
        isDisabled={disabled}
      >
        {options.map((item) => (
          <AutocompleteItem key={item.id}>{item.name}</AutocompleteItem>
        ))}
      </Autocomplete>
    ) : (
      <Select
        {...props}
        selectedKeys={isMulti ? (value ?? []) : value ? [value] : []}
        onSelectionChange={onChange as any}
        selectionMode={isMulti ? 'multiple' : 'single'}
        errorMessage={error?.message}
        isInvalid={invalid}
        isDisabled={disabled}
      >
        {options.map((option) => (
          <SelectItem key={option.id}>{option.name}</SelectItem>
        ))}
      </Select>
    ),
  Textarea: ({ invalid, error, disabled, value, onChange, ...props }) => (
    <Textarea
      {...props}
      value={value ?? ''}
      onValueChange={onChange}
      errorMessage={error?.message}
      isInvalid={invalid}
      isDisabled={disabled}
    />
  ),
  Toggle: ({ value, onChange, label, ...props }) => (
    <Switch {...props} isSelected={!!value} onValueChange={onChange}>
      {label}
    </Switch>
  ),
};

export function ModalProvider({ children }: { children: ReactNode }) {
  return <ComponentState components={modalComponents}>{children}</ComponentState>;
}

2. Add the provider and portal

Wrap your app with ComponentState and add a portal target with the id modal-portal.

Next.js App Router

import type { ReactNode } from 'react';

export default function RootLayout({
  children,
}: Readonly<{ children: ReactNode }>) {
  return (
    <html lang="en">
      <body>
        <ModalProvider>{children}</ModalProvider>
        <div id="modal-portal" />
      </body>
    </html>
  );
}

Next.js Pages Router

import { Html, Head, Main, NextScript } from 'next/document';

export default function Document() {
  return (
    <Html>
      <Head />
      <body>
        <Main />
        <div id="modal-portal" />
        <NextScript />
      </body>
    </Html>
  );
}

3. Render and control the modal

Use useModalHandler to open the modal and render DynamicModal once in your page or component tree.

'use client';

import { DynamicModal, useModalHandler } from 'dynamic-modal';
import { Button } from '@heroui/react';
import simpleModal from './modal-config/simple-modal';

export default function ExamplePage() {
  const { openModal, modalProps } = useModalHandler();

  return (
    <>
      <Button
        onClick={() => {
          openModal(
            simpleModal.default(
              { reserved: 'abc', input1: 'Initial value', store: false },
              (data) => {
                console.log('modal result', data);
              },
            ),
          );
        }}
      >
        Open modal
      </Button>

      <DynamicModal {...modalProps} />
    </>
  );
}

4. Create modal configs

The recommended pattern is to define modal configs with IModalConfigLoader. This lets you:

  • receive input props
  • return a typed modal config
  • receive typed modal output in the action callback

Basic example:

import type { IModalConfigLoader } from 'dynamic-modal';

type IncomingProps = {
  reserved: string;
  input1: string;
  store?: boolean;
  clear?: boolean;
};

type ResultProps = IncomingProps;

const simpleModal: {
  default: IModalConfigLoader<IncomingProps, ResultProps>;
} = {
  default: (props, action) => ({
    reservedData: {
      reserved: props.reserved,
    },
    title: 'Basic modal',
    style: {
      width: '500px',
    },
    fields: [
      {
        elementType: 'input',
        label: 'Input 1',
        name: 'input1',
        defaultValue: props.input1,
        validation: {
          required: true,
          message: 'This field is required',
        },
      },
      {
        elementType: 'group',
        groups: [
          {
            elementType: 'toggle',
            label: 'Store',
            name: 'store',
            defaultValue: `${props.store ?? false}`,
            style: { width: '50%' },
            validation: {
              required: false,
            },
          },
          {
            elementType: 'toggle',
            label: 'Clear',
            name: 'clear',
            defaultValue: `${props.clear ?? false}`,
            style: { width: '50%' },
            validation: {
              required: false,
            },
          },
        ],
      },
    ],
    out: action,
    actions: {
      action: { text: 'Save', color: 'primary' },
      cancel: { text: 'Cancel', color: 'danger' },
    },
  }),
};

export default simpleModal;

Supported field types

You can build modal UIs with these field types:

  • input
  • select
  • textarea
  • toggle
  • text
  • upload
  • custom-upload
  • watcher
  • button
  • table
  • group

group lets you place multiple fields in the same row.

Conditional behavior

One of the main strengths of the library is dynamic behavior based on form state.

renderIf

Use renderIf when a field should appear only if another field matches one or more values.

{
  elementType: 'input',
  label: 'Company name',
  name: 'companyName',
  validation: {
    required: true,
    message: 'Write a company name',
  },
  renderIf: {
    personType: ['company'],
  },
}

You can also use '*' as a wildcard:

renderIf: {
  personType: ['*'],
}

enableIf

Use enableIf when a field should stay visible but only become editable if a condition is met.

{
  elementType: 'input',
  label: 'Discount code',
  name: 'discountCode',
  validation: {
    required: false,
  },
  enableIf: {
    hasDiscount: ['true'],
  },
}

liveData

Use liveData when one field depends on another and must fetch options dynamically.

{
  elementType: 'select',
  label: 'City',
  name: 'cityId',
  options: [],
  validation: {
    required: true,
    message: 'Select a city',
  },
  liveData: {
    condition: ['countryId'],
    action: async (countryId, formData) => {
      const response = await fetch(`/api/cities?countryId=${countryId}`);
      const data = await response.json();

      return data.map((city: { id: string; name: string }) => ({
        id: city.id,
        name: city.name,
      }));
    },
  },
}

watcher

Use watcher when you want to display a derived read-only value built from other fields in the same modal.

watcher listens to the fields listed in watchList, joins their current values, and renders the result using your custom Input component in disabled mode.

Example:

{
  elementType: 'watcher',
  label: 'Full name preview',
  watchList: ['firstName', 'middleName', 'lastName'],
  style: {
    width: '100%',
  },
}

Typical use cases:

  • preview a full name from multiple inputs
  • build a quick summary field for the user
  • show a composed display value without storing it as a real form field

Advanced conditions with async actions

renderIf and enableIf can also use async logic instead of static value maps. This is useful if the decision depends on the backend or on custom business rules.

Example:

renderIf: {
  condition: ['customerId'],
  action: async (customerId, formData) => {
    const response = await fetch(`/api/customers/${customerId}/can-edit`);
    const data = await response.json();
    return data.allowed;
  },
}

The same shape works for enableIf.

Variants and combinations (renderIf, enableIf, liveData)

The library supports these variants:

| Feature | Variant | Shape | | --- | --- | --- | | renderIf | static criteria | renderIf: { fieldName: ['value1', 'value2'] } | | renderIf | wildcard | renderIf: { fieldName: ['*'] } | | renderIf | async action | renderIf: { condition: ['fieldName'], action: async (...) => boolean } | | enableIf | static criteria | enableIf: { fieldName: ['value1', 'value2'] } | | enableIf | wildcard | enableIf: { fieldName: ['*'] } | | enableIf | async action | enableIf: { condition: ['fieldName'], action: async (...) => boolean } | | liveData | single trigger field | liveData: { condition: ['fieldName'], action: async (...) => IOption[] } | | liveData | multiple trigger fields | liveData: { condition: ['fieldA', 'fieldB'], action: async (...) => IOption[] } |

Supported combinations by field type:

  • input, textarea, toggle, upload, custom-upload: renderIf + enableIf
  • select: renderIf + enableIf + liveData
  • table: renderIf + liveData
  • watcher: no renderIf/enableIf/liveData contract in its interface

Behavior note about multiple observed fields:

  • In static mode (Record<field, values>), conditions are evaluated per field-change event.
  • In async mode (condition: [...]), action receives the changed field value as first argument and the whole form as second argument.
  • For liveData, when options refresh, the target field value is reset to its default (defaultValue) or [] in multi-select mode.

Minimal combination example (select with all three):

{
  elementType: 'select',
  label: 'Options',
  name: 'optionId',
  options: [],
  validation: { required: true, message: 'Required' },
  renderIf: { typeId: ['*'] },
  enableIf: { statusId: ['approved'] },
  liveData: {
    condition: ['typeId', 'statusId'],
    action: async (changedValue, formData) => readOptions(changedValue, formData),
  },
}

Examples by use case

1. Basic modal

Use this when you just need a standard modal with fixed fields.

fields: [
  {
    elementType: 'input',
    label: 'Name',
    name: 'name',
    validation: { required: true, message: 'Required' },
  },
  {
    elementType: 'textarea',
    label: 'Description',
    name: 'description',
    validation: { required: false },
  },
];

2. Render fields depending on a select

Use renderIf for mutually exclusive sections.

fields: [
  {
    elementType: 'select',
    label: 'Mode',
    name: 'mode',
    defaultValue: 'email',
    options: [
      { id: 'email', name: 'Email' },
      { id: 'sms', name: 'SMS' },
    ],
    validation: { required: true, message: 'Select a mode' },
  },
  {
    elementType: 'input',
    label: 'Email',
    name: 'email',
    validation: { required: true, message: 'Write an email' },
    renderIf: { mode: ['email'] },
  },
  {
    elementType: 'input',
    label: 'Phone',
    name: 'phone',
    validation: { required: true, message: 'Write a phone' },
    renderIf: { mode: ['sms'] },
  },
];

3. Keep the field visible but disabled

Use enableIf if the user should see the field before it becomes available.

{
  elementType: 'input',
  label: 'Approval note',
  name: 'approvalNote',
  validation: { required: false },
  enableIf: {
    status: ['approved'],
  },
}

4. Load options from another field

Use liveData for dependent selects.

fields: [
  {
    elementType: 'select',
    label: 'Type',
    name: 'typeId',
    options: props.typeList,
    validation: {
      required: true,
      message: 'Please select a valid type',
    },
  },
  {
    elementType: 'select',
    label: 'Options',
    name: 'optionId',
    options: [],
    validation: {
      required: true,
      message: 'Please select a valid option',
    },
    liveData: {
      condition: ['typeId'],
      action: props.optionReadAction,
    },
  },
];

5. Reserve data that should travel with the result

Use reservedData when you want to preserve contextual information without showing it in the modal.

reservedData: {
  customerId: props.customerId,
  source: 'customer-profile',
}

That data will be merged into the object returned by out.

6. Compose a read-only value with watcher

Use watcher when you want the modal to display a value derived from multiple fields while the user types.

fields: [
  {
    elementType: 'input',
    label: 'First name',
    name: 'firstName',
    validation: { required: true, message: 'Required' },
  },
  {
    elementType: 'input',
    label: 'Last name',
    name: 'lastName',
    validation: { required: true, message: 'Required' },
  },
  {
    elementType: 'watcher',
    label: 'Preview',
    watchList: ['firstName', 'lastName'],
    style: { width: '100%' },
  },
];

Important notes:

  • watcher is display-only
  • it does not submit its own value in the modal result
  • it is useful for previews, concatenations, and human-readable summaries

Configuration reference

Modal-level config

Common properties of IModalConfigProps:

  • title: modal title
  • fields: list of modal elements
  • out: callback invoked on submit
  • reservedData: extra data merged into the result
  • onClose: callback when the modal closes
  • style: styles for the modal container
  • overFlowBody: body height/overflow control
  • minHeightBody: minimum body height
  • useSubmit: if false, action button uses manual validation mode
  • useBlur: enables backdrop blur style
  • layout: section-level customization for:
    • container
    • header (showDivider?: boolean)
    • title
    • body
    • footer (showDivider?: boolean) Each section supports className and style.
  • actions.action: main action button props
  • actions.cancel: optional cancel button props
  • actions.containerStyle: style for the action buttons container

Common field properties

Most form fields share:

  • name
  • label
  • placeholder
  • defaultValue
  • style
  • customProperties
  • disabled
  • validation
  • renderIf
  • enableIf

Most field interfaces now also accept native HTML attributes according to the element type (input, textarea, button, select, etc.). These extra props are forwarded with the rest of the field config.

watcher uses:

  • label
  • style
  • customProperties
  • watchList

Validation supports:

  • required
  • message
  • regex
  • maxLength
  • minLength
  • min
  • max

Notes and recommendations

  • Render DynamicModal only once per screen or page branch when possible.
  • Prefer stable name values because they are used to manage form state.
  • Use renderIf for hidden sections and enableIf for visible-but-locked sections.
  • Keep liveData actions fast and deterministic when possible.
  • If your custom UI components use different event contracts, adapt them inside ComponentState rather than changing modal configs.

Repository examples

This repository includes working examples in:

  • examples/simple.ts
  • examples/render-if.ts
  • examples/enable-if.ts
  • examples/live-data.ts

These are useful starting points for building your own modal catalog.