PHPackages                             jacerider/neo\_modal - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. jacerider/neo\_modal

ActiveDrupal-module[Utility &amp; Helpers](/categories/utility)

jacerider/neo\_modal
====================

Provides modal API for elements and fields.

1.0.76(1w ago)02.7k12proprietaryPHP

Since Aug 27Pushed 4d ago1 watchersCompare

[ Source](https://github.com/jacerider/neo_modal)[ Packagist](https://packagist.org/packages/jacerider/neo_modal)[ Docs](https://github.com/jacerider/neo_modal)[ RSS](/packages/jacerider-neo-modal/feed)WikiDiscussions develop Synced 1w ago

READMEChangelogDependencies (16)Versions (78)Used By (2)

CONTENTS OF THIS FILE
---------------------

[](#contents-of-this-file)

- Introduction
- Requirements
- Installation
- Configuration
- Presets
- Usage within TWIG
- Usage within PHP
- Render elements
- Closing a modal
- Groups and galleries
- Usage with AJAX
- Blocks
- Core dialog replacement
- JavaScript API
- Options
- Styling
- Notes and known quirks

INTRODUCTION
------------

[](#introduction)

Provides a modal API for elements and fields. One system covers centered dialogs, left/right/top shelves (off-canvas panels), image/video lightboxes with group navigation, and popovers anchored to another element.

It also **replaces core's jQuery UI dialog**. The module empties `core/drupal.dialog` and `core/drupal.dialog.ajax` and re-points them at its own JavaScript, so existing `data-dialog-type="modal"` links, Views UI, Media Library and Webform dialogs render as Neo modals with no changes on your side, and jQuery UI is never loaded.

For an agent-oriented reference of the same material, see [install/skills/neo-modal/SKILL.md](install/skills/neo-modal/SKILL.md) (copied into `.claude/skills/` by `drush neo:build:install`).

REQUIREMENTS
------------

[](#requirements)

This module requires the Neo suite of modules: `neo`, `neo_settings` and `neo_tooltip`.

INSTALLATION
------------

[](#installation)

Install as you would normally install a contributed Drupal module. Visit  for further information.

CONFIGURATION
-------------

[](#configuration)

Site-wide defaults live at `/admin/config/neo/modal` (permission: *Administer Neo Modal*). The form is grouped into Features, Header, Content, Footer, Group, Size &amp; Placement, Colors and Animations.

Whatever you change here is emitted once per page as `drupalSettings.neoModal.defaults` and becomes the baseline for every modal on the site. Presets and per-modal options are diffed against this baseline and travel as `data-neo-modal-*` attributes on the trigger.

There are four option layers, each overriding the one before it:

1. JavaScript defaults (`src/js/modal/modal.ts`)
2. Site settings (`neo_modal.settings`) → `drupalSettings.neoModal.defaults`
3. A preset (a `neo_settings` variation) → `data-neo-modal-*`
4. Per-modal options → `data-neo-modal-*`

PRESETS
-------

[](#presets)

Presets are `neo_settings` variations of the `neo_modal` plugin, managed on the same settings page. Shipped presets:

IDEffect`dialog`Smart actions, header in content, content scroll. Used automatically for AJAX/core dialogs`media`Numeration, fit, no content padding — the lightbox preset`shelf_left`460px wide, full height, left placement, slide in/out`shelf_right`460px wide, full height, right placement, slide in/out`shelf_top`1200px wide, top placement, fade downReference a preset with or without the `neo_modal_` prefix — `'shelf_right'` and `'neo_modal_shelf_right'` are equivalent. An unknown preset name silently falls back to the active settings.

**Scopes.** A preset with *Allow Scope* checked will additionally merge the preset selected as the site's Frontend or Backend scope (base settings form → Scopes), depending on whether the current route is an admin route. Pass `scope: TRUE` in a modal's options to opt that modal in; the twig filter and function do this by default.

USAGE WITHIN TWIG
-----------------

[](#usage-within-twig)

A twig filter converts a render element into a modal. The subject of the filter is the modal **content**; the first argument is the **trigger**.

```
{{ content|neo_modal(trigger, options, preset, modal_attributes, trigger_attributes) }}
```

In this example the first param is the trigger title, the second is the modal options (because the breakpoint is set to `md`, the modal is only used when the screen is below the medium breakpoint), the third is the preset, and the fourth is the attributes applied to the trigger.

```
{{ form|neo_modal('Filter By'|t, {title: 'Filter Products', breakpoint: 'md'}, 'shelf_left', {class: ['btn btn-outline bg-base-0 w-full text-center']}) }}
```

A gallery of images sharing one group:

```
{% for item in items %}
  {{ item.image|neo_modal(item.thumbnail, {group: 'gallery'}, 'media') }}
{% endfor %}
```

A `neo_modal()` twig **function** also exists, but it hard-codes its content to the string `Modal Content` — it is only useful when the content comes from options (`image`, `video`, `iframe`). Use the filter for real content.

USAGE WITHIN PHP
----------------

[](#usage-within-php)

```
use Drupal\neo_modal\Modal;

$build['link'] = [
  '#type' => 'link',
  '#title' => $this->t('Open'),
  '#url' => Url::fromRoute(''),
];

$modal = new Modal($contentRenderArray, ['title' => 'Details'], 'shelf_right');
$modal
  ->setSubtitle($this->t('More information'))
  ->setColorScheme('primary-dark')
  ->setDraggable(FALSE)
  ->setPlacementToRight();
$modal->applyTo($build['link']);
```

`applyTo()` replaces the build with `['trigger' => …, 'modal' => …]`, where the modal content is wrapped in a `` that the behavior adopts on click.

Option keys may be camelCase or snake\_case — `contentScroll` and `content_scroll` both resolve to `setContentScroll()`.

Media modals need no content at all:

```
$modal = new Modal();
$modal->setTitle('Promo video')
  ->setVideo('https://www.youtube.com/watch?v=9bZkp7q19f0')
  ->setGroup('media')
  ->setTriggerOverlay($this->t('Watch Video'), 'play-circle');
$modal->applyTo($build['thumbnail']);
```

Useful builder methods that have no equivalent options key:

MethodPurpose`setDraggable(bool)`The `drag` option (drag between grouped modals)`setBackdropColor(string)`The `backdropColorBg` option`setTitleWithDynamicIcon(string $title, array $prefix = [])`Title plus an auto-matched icon, and attaches the icon library`setTriggerOverlay($title, $icon, $iconOnly, $attributes)`Hover overlay drawn on top of the trigger`setBreakpoint('md'|'lg')`Render inline above the breakpoint, modal below`addModalClass()` / `setModalClasses()`Classes on `.neo-modal``addTriggerClass()` / `mergeTriggerAttributes()`Trigger markup`applyToForm(&$trigger, &$content)`Form-safe variant of `applyTo()` (div, not template)`getValues()` / `getTriggerAttributes()` / `getAttachments()`Inspect what will be emittedRENDER ELEMENTS
---------------

[](#render-elements)

### neo\_modal

[](#neo_modal)

Wraps its children in a modal.

```
$form['author'] = [
  '#type' => 'neo_modal',
  '#title' => $this->t('Open Modal'),      // the trigger
  '#close' => $this->t('Cancel'),          // optional close button
  '#modal' => [
    'scope' => TRUE,
    'title' => $this->t('Modal Title'),
    'width' => '600px',
  ],
  '#modal_preset' => 'shelf_right',
  '#attributes' => ['class' => ['btn']],           // trigger attributes
  '#wrapper_attributes' => ['class' => ['mt-4']],  // outer wrapper
  '#optional' => TRUE,   // skip rendering when there are no visible children
];

$form['author']['name'] = [
  '#type' => 'textfield',
  '#title' => $this->t('Name'),
];
```

Inside a form the element switches to `applyToForm()`: the content is emitted in a `` rather than a ``, `smartActions` is forced on, and the buttons found in the content are cloned into the modal footer so that submits keep working.

### neo\_modal\_confirm

[](#neo_modal_confirm)

A trigger that opens a confirmation modal with confirm/cancel actions. Defaults to the `shelf_top` preset with `scope` and `smartActions` enabled.

```
$form['delete'] = [
  '#type' => 'neo_modal_confirm',
  '#title' => $this->icon('Delete this thing', 'trash'),
  '#description' => $this->t('Are you sure? This cannot be undone.'),
  '#modal_title' => $this->t('Confirm'),
  '#confirm_text' => $this->t('Delete'),
  '#cancel_text' => $this->t('Cancel'),
  '#submit' => ['::deleteSubmit'],
  '#submit_element' => [],   // extra properties merged into the confirm button
  '#attributes' => ['class' => ['btn']],
];
```

### neo\_modal\_link

[](#neo_modal_link)

Loads a route over AJAX and shows the response in a modal.

```
$build = [
  '#type' => 'neo_modal_link',
  '#title' => $this->t('Edit'),
  '#url' => Url::fromRoute('entity.node.edit_form', ['node' => 1]),
  '#modal' => ['width' => '900px', 'title' => 'Edit'],
  '#modal_preset' => 'dialog',
  '#ajax_url' => $alternateUrl,   // optional: request this instead of #url
  '#ajax_method' => 'GET',        // defaults to POST
];
```

### neo\_modal\_close

[](#neo_modal_close)

Renders a button that closes the modal it lives in.

```
$build['close'] = [
  '#type' => 'neo_modal_close',
  '#title' => $this->t('Close'),
  '#check_ajax' => TRUE,   // only render inside an AJAX/dialog request
];
```

CLOSING A MODAL
---------------

[](#closing-a-modal)

MechanismNotes`data-neo-modal-close` on any elementDeclarative close link/button in your content`#type => 'neo_modal_close'`Render element that builds one`#modal_close => TRUE` on a `submit` elementAdds `data-neo-modal-close-submit`; closes on mouseup so the submit still fires`#close` on a `neo_modal` elementAdds a cancel-style close button`NeoModalCloseCommand`From an AJAX response`Drupal.neoModal.close()`Closes the top modalEscape key / backdrop click`closeOnEscape` and `backdropClose` optionsWith `nest` enabled (the default) the close handlers close the **topmost**modal, which is not necessarily the one that owns the button.

GROUPS AND GALLERIES
--------------------

[](#groups-and-galleries)

Give several triggers the same `group` string and they become one navigable set: prev/next arrows, arrow keys (`navKeyboard`), drag (`drag`) and an optional counter (`numeration`).

Navigation reuses a single modal instance and rebuilds it from the next trigger's options. Only `title`, `subtitle`, `icon`, `image`, `video`, `iframe`, `content` and `trigger` change between slides; every other option stays as it was on the first modal opened.

For fields, the **Neo | Modal Gallery** formatters build the whole thing — `neo_modal_image_gallery` for `image` fields and `neo_modal_media_gallery` for entity reference (media) fields. Both expose thumbnail and full image settings, a modal preset, the title source and the group name.

USAGE WITH AJAX
---------------

[](#usage-with-ajax)

```
use Drupal\neo_modal\Ajax\NeoModalCommand;
use Drupal\neo_modal\Ajax\NeoModalCloseCommand;

$response = new AjaxResponse();
$response->addCommand(new NeoModalCommand($renderArrayOrHtml, [
  'title' => 'Hello',
  'width' => '700px',
  'fit' => TRUE,
]));

// Elsewhere:
$response->addCommand(new NeoModalCloseCommand());
```

The settings array is passed straight through to the JavaScript constructor, so JavaScript-only options work here as well.

Local action links can be turned into modals by adding a `modal` key to the plugin definition — the module swaps in `ModalLocalAction` automatically. Core dialog keys sit at the top level and Neo options go under `neo`:

```
my_module.action:
  route_name: my_module.form
  title: 'Add thing'
  modal:
    width: '900px'
    neo:
      placement: top
      smartActions: true
```

BLOCKS
------

[](#blocks)

Extend `NeoModalBlockBase` (or apply `NeoModalBlockTrait` to an existing block) to get a *Modal Preset* selector, the full modal settings subform, a configurable trigger (text, icon, icon-only, icon position, URL) and an optional **Load modal content via AJAX** mode that renders the block body at `/api/modal/block/{block}/{arg1}/{arg2}` instead of inlining it.

Implement `buildModalContent()`; override `neoModalForceConfiguration()` to hard-lock options that site builders should not change. The module ships `neo_modal_slide_menu` and `neo_modal_account` blocks built this way.

CORE DIALOG REPLACEMENT
-----------------------

[](#core-dialog-replacement)

- `hook_library_info_alter()` blanks `core/drupal.dialog` and `core/drupal.dialog.ajax` and adds dependencies on `neo_modal/modal` and `neo_modal/modal-dialog-ajax`.
- `openDialog`, `closeDialog` and `openModalDialogWithUrl` AJAX commands are reimplemented. `setDialogOption` is a no-op that logs a message.
- `hook_ajax_render_alter()` maps core dialog options (`title`, `width`, `height`, `classes`, `position.my`) onto Neo options and applies per-integration fixes: Media Library and Webform off-canvas dialogs get `width`/`height: 100%` and nesting; Views UI gets full size without nesting.
- Views' `setBrowserUrl` command is suppressed inside `.neo-modal` so modal query parameters cannot leak into `window.location`.
- A jQuery `.dialog()` shim is provided for legacy callers.

JAVASCRIPT API
--------------

[](#javascript-api)

```
// Open an ad-hoc modal.
Drupal.neoModal.open({
  title: 'Hi',
  content: 'Markup, an HTMLElement, or (trigger) => string|HTMLElement',
  width: '600px',
  buttons: { Save: () => doSave(), Cancel: () => Drupal.neoModal.close() },
  onAfterOpen: (modal) => console.log(modal.getContent()),
});

Drupal.neoModal.close();   // close the top modal
Drupal.neoModal.getTop();  // the top NeoModal instance, or null

// Static helpers.
NeoModal.setDefaultOptions(options);
NeoModal.getTop();
NeoModal.closeTop();

// Instance methods.
modal.open();
modal.close();
modal.size();             // re-measure after the content changed
modal.refreshContent();   // rebuild the content footer / smart actions
modal.getModal();
modal.getContent();
modal.getOption('width');
modal.event('onAfterOpen').on(callback);
```

The module registers default `onContentLoaded` and `onAfterClose` handlers that run `Drupal.attachBehaviors()` and `Drupal.detachBehaviors()` on the modal content. Supplying your own handler for those options replaces the default — subscribe with `modal.event(...).on()` instead if you need both.

### `trigger` and `triggerBind`

[](#trigger-and-triggerbind)

`trigger` names the element a modal belongs to. It drives focus restore on close, the argument passed to a `content()` callback, `data-neo-modal-*` option scraping, group navigation and `appendToClosest` — and, separately, a click-to-toggle binding controlled by `triggerBind` (default `true`).

`Drupal.neoModal.open()` forces `triggerBind: false`, because it builds a new instance per call and opens it immediately: keeping the binding would leave a live toggle behind on every call, so the second click would open two modals and the third three. Construct a `new NeoModal({trigger})` **without** calling `open()` when you want the trigger to own the modal — that is what the declarative `.use-neo-modal` behavior does.

Every live modal element exposes its instance as `element.neoModal`.

OPTIONS
-------

[](#options)

Availability legend:

- **PHP** — works as an options array key (`#modal`, twig filter, `new Modal()`)
- **method** — the array key is ignored; call the named method instead
- **Config** — site settings form or preset config only
- **JS** — JavaScript only (`Drupal.neoModal.open()`, `NeoModalCommand`)

### Content and media

[](#content-and-media)

OptionDefaultAvailableNotes`content``null`PHP, JSRender array, string, HTMLElement, or `(trigger) => …` in JS`image``null`PHP, JSImage URL; enables share/download/copy and zoom when `fit``video``null`PHP, JSYouTube/Vimeo URL, or mp4/ogg/webm`iframe``null`PHP, JSArbitrary URL in an iframe`videoAutoplay``true`JS`videoRatio``16x9`JSStyled ratios: `16x9`, `4x3`, `1x1`, `21x9``contentPadding``''`PHP, JSBare numbers gain `px` (`'0'` → `0px`)`contentScroll``false`PHP, JSScroll inside the content instead of the whole modal`fit``false`PHP, JSFit media to the viewport; enables click-to-zoom on images`smartActions``false`PHP, JSClone the form's buttons into the modal footer. Add `btn-ignore` to a button to skip it`buttons``null`JS`{label: callback}` — disables `smartActions`Precedence: `image` → `video` → `iframe` → `content`.

### Header, title and close button

[](#header-title-and-close-button)

OptionDefaultAvailableNotes`header``true`PHP, JSMaster switch for the header row`headerInContent``false`PHP, JSHeader inside the content block instead of floating above it`headerAnimate``true`ConfigAllow title/subtitle/icon/close to animate individually`title``''`PHP, JSAlso `setTitleWithDynamicIcon()``subtitle``''`PHP, JS`icon``''`PHP, JS`setIcon($name)` resolves through neo\_icon; `setRawIcon($selector)` takes a class`iconClasses``neo-icon neo-icon-font`JS`titleCallback``null`Config, JSName of a `window.*` function `(modal, span) => void``closeButton``end`PHP, JS`''`, `start`, `end`, `start-out`, `end-out``closeButtonSvg` / `closeButtonClasses`X icon / `''`JS`numeration``false`PHP, JS"3 / 8" counter — requires `group` with more than one trigger`numerationPlacement``start`JS`start` or `end`### Footer

[](#footer)

The modal footer only renders when there is a media URL to act on — image, video or iframe modals, or content whose first child is an ``/``. Form buttons live in the separate content footer built by `buttons`/`smartActions`.

OptionDefaultAvailableNotes`footer``true`PHP, JS`downloadLink``true`PHP, JS`shareLink``true`PHP, JSRequires `navigator.canShare``copyLink``true`PHP, JSCopies the media URL to the clipboard### Size and placement

[](#size-and-placement)

OptionDefaultAvailableNotes`width``auto`PHP, JS`auto`, `full`, `460px`, `100%`, `80vw`, or a bare number`height``auto`PHP, JSSame forms as width`placement``center`PHP, JS`center`, `top`, `bottom`, `left`, `right`, each also `-start`/`-end``attach``null`PHP, JSCSS selector — turns the modal into a Popper-anchored popover`attachPlacement``auto`PHP, JSOnly emitted when `attach` is also set`appendTo``null`PHP, JSParent for the `.neo-modals` wrapper (default `body`)`appendToClosest``null`PHP, JSNearest ancestor of the trigger; wins over `appendTo``zIndex``null`PHP, JSSets `--modal-z-index``displaceTop`/`Right`/`Bottom`/`Left``''`PHP, JSInsets, e.g. to clear a sticky header`breakpoint`—PHP`md` or `lg`: inline above the breakpoint, modal below### Behavior

[](#behavior)

OptionDefaultAvailableNotes`nest``true`PHP, JSStack on an open modal; `false` closes the current top first`drag``true``setDraggable()`, JSDrag left/right through a group`inputFocus``true`PHP, JSFocus the first form input on open`bodyLock``true`PHP, JSLock body scroll`backdrop``true`PHP, JS`backdropClose``true`JSClick outside to close`closeOnEscape``true`JS`navKeyboard``true`JSArrow keys navigate a group`navPrevLabel` / `navNextLabel``Prev` / `Next`JS`group``null`PHP, JSShared string turns triggers into a gallery`loader``true`ConfigSpinner while content loads`modalClasses``null`PHP, JSClasses on `.neo-modal``wrapperClasses``null`JSClasses on the shared `.neo-modals` wrapper`backdropClasses``null`JS`bodySelector``.page-wrapper`JSElement scaled/blurred by the body transitions`bodyTransitionScale``false`PHP, JSScale the page behind the modal`bodyTransitionBlur``false`PHP, JSBlur the page behind the modal`scope``false`PHPMerge the front/back scope preset; adds `.neo-modal--scoped`### Colors

[](#colors)

Each maps to a `--modal-*` custom property and accepts any CSS color. Available to PHP and JS: `contentColor`, `contentColorBg`, `contentFooterColor`, `contentFooterColorBg`, `headerColor`, `headerColorBg`, `footerColor`, `footerColorBg`, `navColor`, `navColorBg`, `loaderColor`, `loaderColorBg`.

OptionDefaultAvailableNotes`backdropColorBg``''``setBackdropColor()`, JSThe array key is ignored — the setter is named differently`colorScheme``scheme--reset`PHP, JS`setColorScheme($scheme, $inherit = TRUE)`; `primary_dark` → `scheme-primary-dark``colorSchemeInherit``false``setColorScheme()` 2nd arg, JSWhen false the content gets `scheme--reset`### Animations

[](#animations)

Every animated part accepts `AnimateIn`, `AnimateInSpeed`, `AnimateInDelay`, `AnimateOut`, `AnimateOutSpeed` and `AnimateOutDelay` (some parts have no delay). Names come from the Neo animate catalog — `fadeIn*`, `slideIn*`, `zoomIn*`, `bounceIn*`, `flipIn*`, `rotateIn*`, `backIn*`, `lightSpeedIn*`, `rollIn`, `comingIn` and the matching `*Out` variants. Speeds and delays are `slow`, `slower`, `slowest`, `fast`, `faster`, `fastest` (plus `default`). Invalid values are discarded.

PartDefault in / outAvailable`content``comingIn` / `comingOut` at `fastest`PHP, JS`header``slideInDown` / `slideOutUp` at `fastest`PHP, JS`title``fadeInDown` (delay `fastest`) / noneConfig, JS`subtitle``fadeIn` (delay `fast`) / noneConfig, JS`icon``zoomIn` (delay `fastest`) / noneConfig, JS`closeButton``fadeIn` (delay `fast`) / noneConfig, JS`footer``slideInUp` / `slideOutDown` at `fastest`Config, JS`navPrev` / `navNext``slideInLeft` / `slideInRight` …Config, JS`prev` / `next` (between grouped modals)`slideInLeft` / `slideOutLeft` …Config, JS`backdrop``fadeIn` / `fadeOut` at `fastest`Config, JS`loader``fadeIn` / `fadeOut` at `fastest`Config, JS### Callbacks

[](#callbacks)

JavaScript only: `onSettings`, `onBeforeOpen`, `onOpen`, `onAfterOpen`, `onBeforeClose`, `onClose`, `onAfterClose`, `onBeforeNext`, `onNext`, `onAfterNext`, `onBeforePrev`, `onPrev`, `onAfterPrev`, `onContentLoaded` — all `(modal, data?) => void`. The footer icon markup can be overridden with `svgOpen`, `svgClose`, `svgIconDownload`, `svgIconShare` and `svgIconLink`.

STYLING
-------

[](#styling)

- Tailwind variant `modal:` (`.neo-modal &`) styles anything only when it is inside a modal — `class="p-4 modal:p-8"`.
- Spacing tokens `modal-t|r|b|l` and `modal-content-t|r|b|l` map to the live measurements, e.g. `top-modal-t`.
- Custom properties on `.neo-modal`: `--modal-content-color`, `--modal-content-bg`, `--modal-content-footer-bg`, `--modal-header-color`, `--modal-header-bg`, `--modal-footer-color`, `--modal-footer-bg`, `--modal-backdrop-bg`, `--modal-nav-color`, `--modal-nav-bg`, `--modal-loader-color`, `--modal-loader-bg`, `--modal-content-padding`, `--modal-padding-t`, `--modal-z-index`, `--modal-displace-*`, `--modal-max-width`.
- Classes you can add by hand: `neo-modal--btn` (a button smart actions will hoist into the footer), `btn-ignore` (exclude a button from smart actions), `neo-modal--hide`, `neo-modal--tooltip` (gets a tippy on build), `neo-modal--template` (marks content the behavior should adopt).
- State classes: `has-neo-modal`, `neo-modal--body-lock`, `neo-modal--body-transition`, `neo-modal--closing`, `[data-neo-modal--depth]`.

Editing `src/js/**` or `src/css/modal.css` requires a Neo build (`drush neo:build`, or `npm start` while developing).

NOTES AND KNOWN QUIRKS
----------------------

[](#notes-and-known-quirks)

- **Options without a setter are silently dropped.** A key in an options array only takes effect if `Modal` has a matching `set*()` method, the property is in `Modal::getValues()` and the name is in the JavaScript `optionsAsAttributes`list. `drag` and `backdropColorBg` need their setters; `closeOnEscape`, `backdropClose`, `videoRatio`, `videoAutoplay`, `iconClasses`, `titleCallback`, `numerationPlacement`, `nav*Label`, `wrapperClasses`, `backdropClasses`, `bodySelector`, `loader`, `headerAnimate` and the title/subtitle/icon/ closeButton/footer/nav/backdrop/loader animation keys are settings-form or JavaScript territory. Invalid enum values are discarded the same way.
- Config-only options reach the browser through `drupalSettings.neoModal.defaults`, which is built from the **base** settings diff — putting them in a preset or a `#modal` array has no effect.
- `header` and `headerInContent` are mutually exclusive on the wire: if `header`differs from the site default, `headerInContent` is not emitted.
- `appendToClosest` suppresses `appendTo`; `attachPlacement` is only emitted when `attach` is set; `numeration` needs a `group` with more than one trigger.
- The `.neo-modals` wrapper and `.neo-modal--backdrop` are shared by the whole stack, so `wrapperClasses`, `backdropClasses`, `zIndex`, `displace*` and `backdropColorBg` come from whichever modal built the stack first.
- The backdrop animation settings were named `overlayAnimate*` in configuration and schema until `neo_modal_update_11001()` renamed them to `backdropAnimate*`, which is what the form and the JavaScript have always used. Sites that skipped the update keep dead `overlayAnimate*` keys.

Inspect what a modal will actually emit:

```
drush php:eval '$m = new \Drupal\neo_modal\Modal(NULL, ["title" => "T"], "shelf_right"); print json_encode($m->getValues());'
drush php:eval 'print json_encode(\Drupal::service("neo_modal.settings")->getActive()->getDiffConfigValues());'
```

MAINTAINERS
-----------

[](#maintainers)

Current maintainers for Drupal 10:

- Cyle Carlson (jacerider) -

###  Health Score

51

—

FairBetter than 95% of packages

Maintenance99

Actively maintained with recent releases

Popularity24

Limited adoption so far

Community18

Small or concentrated contributor base

Maturity56

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 99.4% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~9 days

Total

77

Last Release

11d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/097ea6514c7dff8bd82162fe88411080a14444a73f86c15ab22eefe28a33059d?d=identicon)[jacerider](/maintainers/jacerider)

---

Top Contributors

[![jacerider](https://avatars.githubusercontent.com/u/638651?v=4)](https://github.com/jacerider "jacerider (154 commits)")[![kazajhodo](https://avatars.githubusercontent.com/u/1173204?v=4)](https://github.com/kazajhodo "kazajhodo (1 commits)")

### Embed Badge

![Health badge](/badges/jacerider-neo-modal/health.svg)

```
[![Health](https://phpackages.com/badges/jacerider-neo-modal/health.svg)](https://phpackages.com/packages/jacerider-neo-modal)
```

###  Alternatives

[farmos/farmos

A web-based farm record keeping application.

1.3k7.3k1](/packages/farmos-farmos)[emulsify-ds/emulsify-drupal

The official Drupal theme for Emulsify, with Storybook and a Vite-based build workflow for generated child themes

96561.6k2](/packages/emulsify-ds-emulsify-drupal)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
