Modals are one of those things that seem straightforward until you start adding real requirements, multiple triggers, loop contexts, auto-open behavior, or repeat open suppression. SN Modal is an Etch component built on the native <dialog> element that handles all of that with just four core props.
Etch has an EtchDialog component in the pattern library, and it’s a fine starting point. However, SN Modal takes a different approach in a few key areas as well as adding some additional functionality. The biggest philosophical difference is where styling lives. EtchDialog handles visual decisions like background color, padding, and border radius through props, which means every instance is configured independently. SN Modal moves all of that into CSS custom properties instead, so your dialogs share a consistent baseline by default and overrides cascade naturally across your site. On top of that, SN Modal adds features EtchDialog doesn’t have: unlimited triggers via event delegation, auto-open via timer or scroll depth, exit-intent detection, and cookie-based repeat open suppression.
How It Compares to EtchDialog
| SN Modal | EtchDialog | |
|---|---|---|
| Props | 4 core functional props + conditional trigger props | 11 props that include a lot of styling options |
| Styling approach | CSS custom properties with empty-variable override pattern | Inline styles via props + CSS |
| Per-instance overrides | Class prop, full CSS cascade | Change props per instance |
| Triggers | Unlimited via data-dialog-open delegation |
Single trigger via querySelector on a selector prop |
| Auto-open | Timer and scroll depth | Not supported |
| Exit intent | Velocity-gated mouse exit detection | Not supported |
| Repeat Open Suppression | Cookie-based suppression | Not supported |
| Programmatic API | window.snModal.open(id) / window.snModal.close(id) |
window.etchElements.etchDialog[id].open() / .close() |
| Loop support | Automatic via matching IDs using dynamic data | Requires isInLoop prop and auto-suffixing logic |
| Open/close transition | CSS @starting-style progressive enhancement |
None |
| Scroll lock | Included | Not supported |
If you’re already using EtchDialog and it’s working for you, there’s no urgent reason to switch. SN Modal is worth reaching for when you need multiple triggers, auto-open behavior, loop contexts, or tighter control over styling consistency across dialogs.
Getting Started
Drop the SN Modal component onto your page and add your content inside the slot. To open the dialog, add data-dialog-open to any element on the page and set the value to match the component’s Dialog ID prop:
<button type="button" data-dialog-open="my-dialog">Open</button>
Set the Dialog ID prop to my-dialog and you’re done. The trigger and dialog are linked by matching IDs, event delegation handles the rest, and you can have as many triggers pointing to the same dialog as you want.
Props
| Label | Type | Default |
|---|---|---|
| Dialog ID | Text | dialog |
| Width | Select | --width-50 |
| Class | Class | — |
| Show / Hide (Editor) | Boolean | false |
| Trigger | Select | button |
Dialog ID
Links triggers to the dialog. Any element with data-dialog-open="your-id" opens the dialog with a matching ID. It must be unique per page when using multiple dialogs.
Width
Controls max-inline-size via a dropdown of ACSS width variables. The dialog always stays within 100% - 2rem regardless of the chosen value, so it’s safe at any screen size. Swap these variables out if you aren’t using ACSS.
Class
For per-instance styling. Assign a class here and use it to override any of the component’s CSS custom properties on that specific dialog. More on this in the Styling section.
Show / Hide (Editor)
Toggles the dialog open or closed on the Etch canvas so you can edit the content without clicking a trigger. Has no effect on the frontend.
Trigger
Controls how the dialog opens.
| Label | Behavior |
|---|---|
| Button | Opens only via [data-dialog-open] triggers or the JS API |
| Additional Trigger Selector | When Button is selected, this conditional prop gives you another way to trigger the modal |
| Auto | Opens automatically via timer or scroll depth |
| Exit Intent | Opens when the user’s mouse exits quickly through the top of the viewport |
Auto Trigger
When Trigger is set to auto, additional props appear conditionally.
| Label | Type | Default | Notes |
|---|---|---|---|
| Auto Trigger | Select | timer |
Select from Timer or Scroll Depth |
| Delay | Text | 5 |
If you select Timer, then you will put the number of seconds until it triggers after page load |
| Scroll Depth | Text | 500 |
If you select Scroll Depth, you will put the number of px the user has to scroll to trigger the modal |
Timer opens the dialog after a set number of seconds. Scroll Depth opens it once window.scrollY reaches the configured pixel value. If the page is shorter than the target, the dialog fires when the user reaches the bottom instead.
Additional Trigger Selector
An optional CSS selector for triggering the dialog from elements you can’t add data-dialog-open to, like third-party plugin output, menu items, or other markup outside your control. Accepts any valid CSS selector (class, ID, attribute, etc.) and supports multiple matches.
<!-- Examples -->
.third-party-cta
#legacy-button
[data-action="open-contact"]
Exit Intent
When Trigger is set to exit-intent, the dialog opens when the user moves their mouse quickly out through the top of the viewport, toward the browser tabs or URL bar.
This is desktop only by nature. Touch devices have no mouse signal, so the trigger simply never fires there. The velocity gate filters out slow drifts (like moving to a second monitor or reaching for a bookmark), so only a fast upward exit counts.
Repeat Open Suppression
When Trigger is set to auto or exit-intent, cookie props appear conditionally.
| Label | Type | Default |
|---|---|---|
| Don’t Show Again | Boolean | true |
| Hide For (days) | Text | 1 |
When enabled, a cookie (sn_modal_seen_<dialogId>) is written when the user closes an auto-opened dialog. While that cookie exists, the auto or exit-intent trigger won’t fire again. Manual [data-dialog-open] button triggers are never affected by the cookie.
The cookie is written on close, so it only kicks in after the user has actually seen and dismissed the dialog.
To reset during testing, go to DevTools → Application → Cookies and delete sn_modal_seen_*.
Closing the Dialog
You can close the dialog in the following ways:
- Esc closes the dialog (native browser behavior, always works)
- Backdrop click closes the dialog. Uses the native
closedby="any"attribute where supported, with a JS polyfill for older browsers - Close button (the × in the corner) closes the dialog
- Programmatically via
window.snModal.close('dialog-id')from any snippet on the page
Closing from a Form
Form plugins like Fluent Forms, WS Form, or Gravity Forms handle submission via Ajax, so the dialog needs to be closed programmatically once the submission completes. Hook into your form plugin’s success event and call the JS API. For example, with Fluent Forms:
jQuery(document).on('fluentform_submission_success', function() {
window.snModal.close('your-dialog-id');
});
Check your form plugin’s documentation for the equivalent success event. The pattern is the same regardless of which plugin you’re using.
Programmatic API
Any dialog can be opened or closed from another snippet using the public API:
window.snModal.open('contact');
window.snModal.close('contact');
The full registry is available at window.snModal.registry, which is a Map keyed by dialog ID.
Using in Loops
Inside an Etch loop, map both the trigger and the dialog ID to the same loop field. Each pair links automatically.
Trigger and Modal (inside the loop):
<button type="button" data-dialog-open="modal-{item.id}">More Details</button>
<SnModal dialogId="modal-{item.id}">
{#slot default}
<p>Here is my modal Content</p>
{/slot}
</SnModal>
Each loop iteration produces a unique trigger/dialog pair. No additional configuration needed.
Styling
CSS Custom Properties
All visual values are controlled through locally-scoped custom properties with empty-variable overrides and fallback defaults. The public --sn-modal-* variables are what you set to customize the component.
| Variable | Default | Description |
|---|---|---|
--sn-modal-bg |
white |
Dialog background |
--sn-modal-padding |
var(--space-l, 3rem) |
Inner content padding |
--sn-modal-backdrop |
rgb(0 0 0 / 0.5) |
Backdrop overlay color |
--sn-modal-radius |
var(--radius, 0.5rem) |
Border radius |
--sn-modal-close-offset |
0.5rem |
Close button position |
--sn-modal-close-size |
20px |
Close button icon size |
--sn-modal-close-bg |
transparent |
Close button background |
--sn-modal-close-radius |
100vw |
Close button radius |
Global Customization
The component includes .sn-modal--base in the Class prop by default. It’s pre-wired with all the customization variables as commented-out placeholders. Uncomment and change any value you want to apply across all your modals.
.sn-modal--base {
/* --sn-modal-bg: white; */
/* --sn-modal-padding: var(--space-l); */
/* --sn-modal-backdrop: rgb(0 0 0 / 0.5); */
/* --sn-modal-radius: var(--radius, 0.5rem); */
/* --sn-modal-close-offset: 0.5rem; */
/* --sn-modal-close-size: 20px; */
/* --sn-modal-close-bg: transparent; */
/* --sn-modal-close-color: black; */
/* --sn-modal-close-radius: 100vw; */
}
Per-Instance Override
To style a specific dialog differently, add a second class via the Class prop alongside .sn-modal--base and override whichever variables you need:
.dialog--dark {
--sn-modal-bg: var(--color-dark);
--sn-modal-backdrop: rgb(0 0 0 / 0.8);
}
Close Button: Inside vs. Outside
The close button position is controlled entirely by --sn-modal-close-offset:
- Positive values (default
0.5rem): button sits inside the dialog - Negative values: button sits outside the dialog
When positioning the button outside, you’ll typically want a visible background. Set these in .sn-modal--base:
.sn-modal--base {
--sn-modal-close-offset: -0.75rem;
--sn-modal-close-bg: white;
}
Open/Close Transition
The component includes a CSS transition using @starting-style and transition-behavior: allow-discrete. Browsers that support it get a smooth fade-and-slide; browsers that don’t simply snap open and closed.
Scroll Lock
Scroll lock is handled automatically via a nested CSS rule inside .sn-modal.
Accessibility
- Focus trapping is handled natively by
showModal(). Tab and Shift+Tab cycle through focusable elements inside the dialog. - When the dialog closes, focus returns to the element that opened it.
- Esc to close
- The close button has
aria-label="Close popup". The SVG icon isaria-hidden="true"andfocusable="false". showModal()makes the background inert, so screen readers can’t interact with content behind the dialog.
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
| Trigger does nothing | data-dialog-open value doesn’t match dialogId |
Check for typos or whitespace |
| Auto trigger doesn’t fire | Cookie from a previous test | sn_modal_seen_ → Application → Cookies → delete sn_modal_seen_* |
| Backdrop click doesn’t close | Padding on the <dialog> element |
Padding goes on .sn-modal__body, not .sn-modal |
| Dialog not centered | CSS reset overriding margin: auto |
Ensure .sn-modal has margin: auto |
| Exit intent never fires | Mouse exited through the side, or too slowly | Must exit through the top of the viewport with a fast upward motion |