# ๐Ÿฌ swal_rails **SweetAlert2 v11 for Rails 7+ โ€” batteries included.** First-class support for **Importmap**, **jsbundling**, and **Sprockets**, with a Stimulus controller, auto-wired flash messages, Turbo confirm replacement, Ruby view helpers, and full I18n. Everything is configurable. [![CI](https://github.com/Metalzoid/swal_rails/actions/workflows/main.yml/badge.svg)](https://github.com/Metalzoid/swal_rails/actions) [![Gem Version](https://badge.fury.io/rb/swal_rails.svg)](https://rubygems.org/gems/swal_rails) [![Ruby](https://img.shields.io/gem/ruby-version/swal_rails?label=ruby)](https://www.ruby-lang.org/) [![Rails](https://img.shields.io/gem/dv/swal_rails/railties?label=rails)](https://rubyonrails.org/) [![SweetAlert2](https://img.shields.io/badge/SweetAlert2-v11.26-3085d6.svg)](https://sweetalert2.github.io/) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE.txt)

๐Ÿ“– Table of contents


๐Ÿค” Why swal_rails?

The existing gems haven't shipped a release since 2019 (SA2 was on v7 back then โ€” it's on v11 now) and were built for the Rails 5 / UJS era. swal_rails is the modern replacement:

sweetalert2-rails sweetify swal_rails
SweetAlert2 v11.x โŒ โŒ โœ…
Rails 7+ / Turbo-native โŒ โŒ โœ…
Importmap โŒ โŒ โœ…
jsbundling (esbuild / vite / rollup) โŒ โŒ โœ…
Sprockets โœ… โŒ โœ…
Stimulus controller โŒ โŒ โœ…
Flash auto-wire, map per key โŒ partial โœ…
Turbo setConfirmMethod override โŒ โŒ โœ…
data-swal-confirm attribute โŒ โŒ โœ…
Ruby view helpers (swal_tag) โŒ โŒ โœ…
I18n Rails (fr/en shipped) โŒ โŒ โœ…
a11y (reduced-motion, ARIA) โŒ โŒ โœ…
Last release 2019 2019 maintained

โœจ Features

  • ๐ŸŽจ SweetAlert2 v11 vendored and pinned โ€” no CDN, no surprise upgrades.
  • โšก Three asset pipelines: Importmap (default), jsbundling, Sprockets.
  • ๐Ÿ”” Auto-wired flash โ€” flash[:notice] โ†’ toast, flash[:alert] โ†’ modal, fully mappable per key.
  • ๐Ÿ›ก๏ธ Turbo confirmations โ€” replace the native confirm() globally or opt-in per element.
  • ๐ŸŽฎ Stimulus controller (data-controller="swal") for declarative popups.
  • ๐Ÿงฑ Ruby view helpers โ€” swal_tag, swal_config_meta_tag, swal_flash_meta_tag.
  • ๐Ÿ”’ CSP-friendly โ€” swal_tag(..., nonce: true) propagates the per-request nonce.
  • ๐ŸŒ I18n ready โ€” en / fr locales shipped, override freely.
  • โ™ฟ Accessibility โ€” honors prefers-reduced-motion, preserves ARIA & focus trap.
  • ๐Ÿงฉ Engine-based โ€” zero monkey-patching, follows Rails Engine conventions.

๐Ÿ”ง Compatibility

Tested on every combination of Ruby and Rails listed below via the Appraisal gem:

Rails 7.2 Rails 8.0 Rails 8.1.3 Rails 8.1.3 + Sprockets
Ruby 3.2 โœ… โœ… โœ… โœ…
Ruby 3.3 โœ… โœ… โœ… โœ…
Ruby 3.4 โœ… โœ… โœ… โœ…
Ruby 3.5 โœ… โœ… โœ… โœ…
Ruby 4.0 โ€” โœ… โœ… โœ…

Requirements: Ruby โ‰ฅ 3.2 (tested up to 4.0), Rails โ‰ฅ 7.2 (tested up to 8.1.3), Turbo recommended.


๐Ÿ“ฆ Installation

Add this line to your application's Gemfile:

gem "swal_rails"

During the beta, pin the prerelease explicitly:

gem "swal_rails", "0.3.1.beta1"

or install globally with gem install swal_rails --pre. Bundler ignores prereleases unless you ask for one.

Then install and run the generator:

$ bundle install
$ bin/rails g swal_rails:install

The generator autodetects your asset pipeline. You can force it:

$ bin/rails g swal_rails:install --mode=importmap   # default for new Rails apps
$ bin/rails g swal_rails:install --mode=jsbundling  # esbuild, vite, rollup
$ bin/rails g swal_rails:install --mode=sprockets   # legacy apps

What the generator does

  • ๐Ÿ“„ creates config/initializers/swal_rails.rb
  • ๐Ÿ“Œ pins (Importmap) or adds (jsbundling/sprockets) sweetalert2 and swal_rails
  • ๐Ÿ’‰ injects <%= swal_rails_meta_tags %> into app/views/layouts/application.html.erb
  • ๐ŸŒ loads the en / fr locales

Finally, in your JS entrypoint (e.g. app/javascript/application.js):

import "swal_rails"

๐Ÿš€ Quick start

Thirty seconds, tops. After running the installer:

# app/controllers/posts_controller.rb
def create
  @post = Post.create!(post_params)
  redirect_to @post, notice: "Post created!"
end
<%# app/views/posts/show.html.erb %>
<%= button_to "Delete", @post, method: :delete,
      data: { turbo_confirm: "Really delete this post?" } %>

That's it. notice renders as a toast, the delete button opens a SweetAlert2 modal instead of the browser's ugly native confirm().


โš™๏ธ Configuration

Everything lives in config/initializers/swal_rails.rb:

SwalRails.configure do |config|
  # โ”€โ”€โ”€ Confirm behavior โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  # :off            โ†’ don't touch Turbo, no data-attribute listener
  # :data_attribute โ†’ only intercept [data-swal-confirm] clicks (default)
  # :turbo_override โ†’ replace Turbo.setConfirmMethod globally
  # :both           โ†’ data-attribute + Turbo override
  config.confirm_mode = :data_attribute

  # โ”€โ”€โ”€ UX โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  config.respect_reduced_motion = true   # disable animations when OS asks
  config.expose_window_swal     = true   # window.Swal for console hacking

  # โ”€โ”€โ”€ Defaults passed to every Swal.fire call โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  config.default_options = {
    buttonsStyling: true,
    reverseButtons: false,
    focusConfirm:   true,
    returnFocus:    true
  }

  # โ”€โ”€โ”€ Flash โ†’ Swal mapping (per key) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  config.flash_map[:notice]  = { icon: "success", toast: true,  position: "top-end", timer: 3000 }
  config.flash_map[:success] = { icon: "success", toast: true,  position: "top-end", timer: 3000 }
  config.flash_map[:alert]   = { icon: "error",   toast: false }
  config.flash_map[:error]   = { icon: "error",   toast: false }
  config.flash_map[:warning] = { icon: "warning", toast: true,  position: "top-end", timer: 4000 }
  config.flash_map[:info]    = { icon: "info",    toast: true,  position: "top-end", timer: 3000 }

  # โ”€โ”€โ”€ I18n scope (for button labels) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  config.i18n_scope = "swal_rails"
end

๐Ÿ’ก Per-key override tip: to disable the toast for a single key without rewriting the whole map: config.flash_map[:alert] = { icon: "error", toast: false }.


๐ŸŽฏ Usage

Flash messages

Any flash set from a controller is rendered automatically on page load:

flash[:notice] = "Profile updated"   # โ†’ toast top-right
flash[:alert]  = "Could not save"    # โ†’ modal

Arrays are expanded into one popup per message โ€” handy for model errors:

flash[:alert] = @post.errors.full_messages  # ["Title can't be blank", "Body is too short"]
# โ†’ two separate Swals, one per message

Need to override the per-key defaults for a single request? Assign a Hash instead of a String โ€” its keys flow straight into Swal.fire and shadow flash_map[key]:

flash[:notice] = { text: "Deployed!", icon: "rocket", timer: 5000, toast: true }
# โ†’ ignores flash_map[:notice], fires a 5-second rocket toast

Behind the scenes, the engine serializes the flash into a meta tag (<meta name="swal-flash" content="...">) and the JS runtime reads it and calls Swal.fire(...) with your per-key options.


Turbo confirmations

Works with standard Rails / Turbo syntax:

<%= button_to "Delete", post_path(@post), method: :delete,
      data: { turbo_confirm: "Really delete?" } %>

When confirm_mode is :turbo_override or :both, swal_rails replaces Turbo.setConfirmMethod with a SweetAlert2-backed implementation โ€” the same data-turbo-confirm attribute now shows a proper modal.

Pass a Hash instead of a string to carry full SA2 options:

<%= button_to "Delete", post_path(@post), method: :delete, data: {
      turbo_confirm: { icon: "error", title: "Really?", confirmButtonText: "Nuke" }
    } %>

Rails JSON-encodes the Hash into the attribute; the runtime parses it back and treats it as a full options object (same thing works with data-swal-confirm).


data-swal-confirm attribute

If you don't want to override Turbo globally, opt-in per element:

<%= link_to "Archive", archive_path,
      data: { swal_confirm: "Archive this item?", swal_icon: "warning" } %>

Supported data attributes:

Attribute Maps to
data-swal-confirm text / title prompt
data-swal-title title
data-swal-text text
data-swal-icon icon
data-swal-confirm-text confirmButtonText
data-swal-cancel-text cancelButtonText
data-swal-options (JSON) full SA2 options (wins over the above)

Use data-swal-options when you need anything beyond the shortcuts โ€” it accepts any SweetAlert2 option:

<%= button_to "Delete", post_path(@post), method: :delete, data: {
      swal_confirm: "Danger",
      swal_options: { icon: "error", iconColor: "#ff0000", confirmButtonText: "Nuke" }.to_json
    } %>

Multi-step confirmations

For destructive flows (account deletion, legal opt-ins, irreversible actions), chain several popups via data-swal-steps. Each step only fires if the previous one was confirmed โ€” any Cancel or Esc aborts the whole cascade, and the original click/submit never reaches the server:

<%= button_to "Delete account", account_path, method: :delete, data: {
      swal_steps: [
        { title: "Delete your account?", icon: "warning" },
        { title: "This cannot be undone", icon: "error" },
        { title: "Type DELETE to confirm", input: "text" }
      ].to_json
    } %>

Every step is a full SweetAlert2 options Hash โ€” override the default icon, buttons, timer, input: type, anything SA2 accepts. The per-step defaults (showCancelButton: true, focusCancel: true, icon: "warning") are merged in first and can be replaced key-by-key.

Conditional branching (onConfirmed / onDenied)

Add a Deny button (showDenyButton: true) to get a three-way choice, and attach a nested sub-chain to either outcome:

<%= button_to "Delete or disable?", account_path, method: :delete, data: {
      swal_steps: [
        {
          title: "Delete or just disable?",
          icon: "question",
          showDenyButton: true,
          confirmButtonText: "Delete forever",
          denyButtonText:    "Disable for 30 days",
          onDenied: [
            { title: "Confirm disable", icon: "info" }
          ]
        }
      ].to_json
    } %>

Semantic rules, per step:

SA2 result Behavior
isDismissed Abort the entire chain; action does not fire
isConfirmed Run onConfirmed sub-chain if present (replaces remainder); else continue linearly
isDenied Run onDenied sub-chain if present (its result decides); else abort

Sub-chains are recursive โ€” they're just nested arrays of steps.

Under confirm_mode = :turbo_override (or :both), passing a JSON array to data-turbo-confirm works the same way:

<%= button_to "Delete", account_path, method: :delete, data: {
      turbo_confirm: [
        { title: "Really?" },
        { title: "Really really?" }
      ]
    } %>

From Ruby, the view helper swal_chain_tag fires a chain inline on page load (same CSP nonce and XSS hardening as swal_tag):

<%= swal_chain_tag([
      { title: "Welcome back" },
      { title: "Accept updated terms?" }
    ]) %>

Stimulus controller

For fully declarative popups without touching JS:

<button data-controller="swal"
        data-action="click->swal#fire"
        data-swal-options-value='{"title":"Hello","icon":"success"}'>
  Ping
</button>

Available actions: fire, confirm, chain. The chain action reads data-swal-steps-value (same shape as data-swal-steps) and submits the enclosing form if every step resolves confirmed.


Ruby view helpers

Fire a one-shot popup directly from a view:

<%= swal_tag(title: "Welcome back!", icon: "info", timer: 2000) %>

Under a strict Content Security Policy, pass nonce: true โ€” Rails fills in the per-request nonce so the inline <script> survives the policy:

<%= swal_tag({ title: "Welcome back!" }, nonce: true) %>

Heads-up: the emitted tag is <script type="module"> with a bare import Swal from "sweetalert2". That resolves via Importmap (or any shim that processes import maps). On a pure esbuild/webpack setup with no importmap tag on the page, prefer the Stimulus controller or call window.Swal.fire(...) from your bundle instead.

Lower-level helpers (injected by the generator into your layout):

<%= swal_rails_meta_tags %>
<%# expands to: %>
<%= swal_config_meta_tag %>  <%# serializes SwalRails.configuration %>
<%= swal_flash_meta_tag %>   <%# serializes current flash, if any %>

Programmatic JS

Swal is re-exported from the gem's JS runtime:

import Swal from "sweetalert2"

Swal.fire({
  title: "Saved!",
  icon: "success",
  toast: true,
  position: "top-end",
  timer: 3000
})

If config.expose_window_swal = true, window.Swal is also available for quick console debugging.


๐Ÿ“˜ Reference

Complete, at-a-glance specification of every public surface the gem exposes. The sections above give narrative walk-throughs โ€” this section is the lookup table.

SwalRails.configure

SwalRails.configure { |config| ... }       # block form, yields Configuration
SwalRails.configuration                    # reader โ€” memoized, safe to mutate
SwalRails.reset_configuration!             # resets to defaults (test fixture helper)

Configuration attributes

Attribute Type Default Description
confirm_mode Symbol :data_attribute Routing of confirm dialogs โ€” see values below. Validated: assignment with any other value raises ArgumentError. Strings are coerced to symbols.
flash_keys_as_meta Boolean true When false, swal_flash_meta_tag returns nil โ€” useful to opt out without removing the swal_rails_meta_tags call from the layout.
respect_reduced_motion Boolean true When the OS reports prefers-reduced-motion: reduce, the gem empties SA2's showClass / hideClass to suppress animations.
expose_window_swal Boolean true When true, window.Swal is set to the mixed-in Swal instance after boot (useful for console debugging and inline scripts).
default_options Hash see below Merged into every Swal.fire(...) call via Swal.mixin(...).
flash_map Hash see below Flash-key โ†’ SA2 options mapping. Keys normalized to symbols. Non-Hash assignment raises ArgumentError.
i18n_scope String "swal_rails" I18n scope used to look up confirm_button_text, cancel_button_text, deny_button_text, close_button_aria_label. Non-string values are coerced.

confirm_mode accepted values:

Value Behavior
:off No Turbo override, no data-attribute listener. Use Swal manually.
:data_attribute (default) intercept clicks/submits on [data-swal-confirm] and [data-swal-steps]. Does not touch Turbo.
:turbo_override Replaces Turbo.setConfirmMethod globally. data-turbo-confirm attributes open SA2 modals.
:both Enables both mechanisms โ€” useful for mixed codebases migrating over.

default_options default:

{ buttonsStyling: true, reverseButtons: false, focusConfirm: true, returnFocus: true }

flash_map default (per key, all overridable):

{
  notice:  { icon: "success", toast: true,  position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false },
  success: { icon: "success", toast: true,  position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false },
  alert:   { icon: "error",   toast: false, timer: nil },
  error:   { icon: "error",   toast: false, timer: nil },
  warning: { icon: "warning", toast: true,  position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false },
  info:    { icon: "info",    toast: true,  position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
}

to_client_payload (internal, read-only)

Serialization contract consumed by the JS runtime via the <meta name="swal-config"> tag. Returns:

{
  confirmMode:          Symbol,
  respectReducedMotion: Boolean,
  exposeWindowSwal:     Boolean,
  defaultOptions:       Hash,
  flashMap:             Hash,
  i18n:                 Hash   # only keys whose translation is present
}

View helpers

All helpers are injected into both ActionController::Base and ActionView::Base by the engine; they are available in every view and in every controller-side rendering context.

Helper Returns Description
swal_rails_meta_tags ActiveSupport::SafeBuffer Emits swal_config_meta_tag + swal_flash_meta_tag joined with "\n". Call once, in <head>.
swal_config_meta_tag ActiveSupport::SafeBuffer Emits <meta name="swal-config" content="โ€ฆJSONโ€ฆ"> with the serialized SwalRails.configuration.to_client_payload.
swal_flash_meta_tag ActiveSupport::SafeBuffer or nil Emits <meta name="swal-flash" content="โ€ฆJSONโ€ฆ"> if flash_keys_as_meta is enabled and the current flash is non-empty; otherwise returns nil.
swal_tag(options, html_options) ActiveSupport::SafeBuffer Inline <script type="module"> firing Swal.fire(options) once. Options are JSON-escaped to neutralize </script>, <!--, U+2028, U+2029.
swal_chain_tag(steps, html_options) ActiveSupport::SafeBuffer Inline <script type="module"> firing chainDialogs(Swal, steps). steps is an Array of Hashes (single Hash is auto-wrapped). Same escape hardening as swal_tag.

html_options (for swal_tag / swal_chain_tag)

  • nonce: true โ†’ propagates the per-request CSP nonce (when Rails exposes content_security_policy_nonce). Silently dropped when no CSP helper is configured, so the helper stays safe in apps without CSP.
  • Any other key is forwarded to javascript_tag as <script> attributes.

The default type is "module" โ€” override with html_options = { type: "text/javascript" } if you need a classic script.

โš ๏ธ The emitted <script type="module"> contains bare imports (import Swal from "sweetalert2"). These resolve via Importmap; in a pure esbuild/webpack setup with no importmap tag on the page, call window.Swal.fire(...) from your bundle instead.


Data attributes

All attributes are read from the element's dataset. Values carry through button_to, link_to, form_with, and raw HTML equally.

Single-step confirm (data-swal-confirm)

Attribute Accepts Maps to (SA2 option)
data-swal-confirm String or JSON object or JSON array message (String) โ†’ text; Object โ†’ full SA2 options (overrides all shortcuts); Array โ†’ multi-step chain (see below)
data-swal-title String title
data-swal-text String text
data-swal-icon String ("warning", "error", โ€ฆ) icon (default "warning")
data-swal-confirm-text String confirmButtonText
data-swal-cancel-text String cancelButtonText
data-swal-options JSON Object (stringified) Full SA2 options โ€” wins over all shortcuts and over the JSON object form of data-swal-confirm

Merge order (later wins):

defaults โ†’ data-swal-* shortcuts โ†’ JSON object in data-swal-confirm โ†’ data-swal-options

Multi-step chain (data-swal-steps)

Attribute Accepts Behavior
data-swal-steps JSON Array of step Hashes (stringified) Runs the chain. Per-step defaults { showCancelButton: true, focusCancel: true, icon: "warning" } are merged first and can be overridden key-by-key. Every step is a full SA2 options Hash plus the optional onConfirmed / onDenied sub-chain keys.

Both attributes coexist โ€” if data-swal-steps is present and non-empty, it takes precedence over data-swal-confirm.

Turbo data-turbo-confirm

When confirm_mode is :turbo_override or :both, data-turbo-confirm accepts:

Form Behavior
String SA2 popup with the string as text.
Hash (JSON-encoded by Rails) Full SA2 options.
Array (JSON-encoded by Rails) Multi-step chain โ€” same shape as data-swal-steps.

Stimulus controller reference

Registered under the identifier "swal".

Values

Value Type Default Used by
optionsValue Object {} fire, confirm
stepsValue Array [] chain

Actions

Action Target behavior
fire Fires Swal.fire(optionsValue). Calls preventDefault() on the event if the element is <a> or <button>. Returns the SA2 promise.
confirm Fires Swal.fire({ showCancelButton: true, focusCancel: true, ...optionsValue }). On isConfirmed, calls requestSubmit() (fallback submit()) on the enclosing form.
chain Runs chainDialogs(Swal, stepsValue). On resolved true, calls requestSubmit() / submit() on the enclosing form. Returns the boolean.

Example:

<button data-controller="swal"
        data-action="click->swal#chain"
        data-swal-steps-value='[{"title":"Sure?"},{"title":"Really?"}]'>
  Proceed
</button>

JS runtime

Entry point

import "swal_rails"              // installs confirm + flash handlers

Expected in your JS entrypoint (e.g. app/javascript/application.js).

Re-exports

import Swal from "sweetalert2"                            // SA2, re-exported as default from swal_rails
import { Swal } from "swal_rails"                         // named re-export (same instance)
import { chainDialogs, CHAIN_DEFAULTS } from "swal_rails/chain"
import { installConfirm } from "swal_rails/confirm"       // for custom boot sequences
import { installFlash } from "swal_rails/flash"           // same

chainDialogs(Swal, steps)

chainDialogs(Swal: typeof import("sweetalert2"), steps: Array<StepOptions>): Promise<boolean>

Returns true iff a complete path through the chain was confirmed. steps may be empty (resolves true immediately). Non-array input resolves true as well.

StepOptions is any valid SA2 options Hash, plus the two chain-only keys:

Key Type Effect
onConfirmed Array<StepOptions> On isConfirmed, run this sub-chain and adopt its boolean result (replaces the remainder of the current chain).
onDenied Array<StepOptions> On isDenied (requires showDenyButton: true), run this sub-chain and adopt its boolean result. Without this key, isDenied aborts the chain.

Events

Event name Target event.detail When
swal-rails:ready document { Swal, config } Fired once per page lifetime after the first successful boot (not per Turbo navigation).

Meta-tag contract

Meta name Payload Read by
swal-config to_client_payload JSON Runtime boot โ€” mixin, confirm handler, flash handler. Once per page.
swal-flash Array of { key, options } Flash runtime โ€” re-read on every turbo:load.

Flash entries are { key: "notice", options: { text: "..." } } for string values, or { key: "notice", options: {...user hash...} } for Hash values. Arrays in flash[key] are expanded into one entry per element.


Generators

bin/rails g swal_rails:install

Flag Type Default Values
--mode String auto auto, importmap, jsbundling, sprockets
--confirm_mode String data_attribute off, data_attribute, turbo_override, both โ€” baked into the generated initializer
--skip_layout Boolean false When set, does not inject <%= swal_rails_meta_tags %> into app/views/layouts/application.html.erb

--mode=auto detection order:

  1. config/importmap.rb present โ†’ importmap
  2. package.json present โ†’ jsbundling
  3. fallback โ†’ sprockets

Per-mode side effects:

  • importmap: appends pin "sweetalert2", to: "sweetalert2.esm.all.js" and pin "swal_rails", to: "swal_rails/index.js" to config/importmap.rb; appends import "swal_rails" to app/javascript/application.js.
  • jsbundling: runs yarn add sweetalert2@<pinned> or npm install sweetalert2@<pinned> (based on the lockfile present); appends import "swal_rails" to app/javascript/application.js.
  • sprockets: appends //= link sweetalert2.js and //= link sweetalert2.css to app/assets/config/manifest.js.

All append operations are idempotent โ€” running the generator twice is safe.

bin/rails g swal_rails:locales

No flags. Copies config/locales/swal_rails.en.yml and swal_rails.fr.yml from the gem into your app's config/locales/.


Flash value shapes

Value type Rendered as
String { text: value } โ€” safe by default (SA2 renders via text:, no HTML injection).
Hash Full SA2 options, shadows flash_map[key]. Any SA2 key is accepted (icon, timer, input, html, iconHtml, โ€ฆ).
Array Expanded into one entry per element. Strings become { text: elem }, Hashes pass through verbatim.
nil / "" / blank? Skipped.

Key normalization: Hash keys are symbolize_keys-ed before serialization, so flash[:notice] = { "text" => "..." } and flash[:notice] = { text: "..." } are equivalent.

โš ๏ธ Hash overrides bypass the text: safety net. Using html:, iconHtml:, or footer: with untrusted input is an XSS โ€” SweetAlert2 renders those as raw HTML by design. Rule of thumb: if the value is user-controlled, keep the String form.


Chain semantics

Single source of truth for every chain-aware entry point (data-swal-steps, data-turbo-confirm with an array, Stimulus chain action, swal_chain_tag, direct chainDialogs call).

Per step:

SA2 result Behavior
isDismissed (ร—, Esc, backdrop) Abort the current chain โ†’ false. Outer chain (if this was a sub-chain) also terminates with false.
isConfirmed If onConfirmed: [...] is defined, run it recursively and replace the remainder of the current chain with its result. Else continue linearly.
isDenied (requires showDenyButton: true) If onDenied: [...] is defined, run it recursively and adopt its result. Else abort โ†’ false.

Return rules:

  • An empty or non-array steps input resolves true immediately.
  • A chain resolves true iff it ran to completion along a path without any dismiss or unbranched deny.
  • Sub-chains inherit the same per-step defaults (showCancelButton: true, focusCancel: true, icon: "warning").
  • onConfirmed / onDenied keys are stripped before being passed to Swal.fire, so they never leak into the SA2 popup options.

Callback contract for Turbo override: the chain's final Promise<boolean> is what Turbo.setConfirmMethod receives โ€” false cancels the navigation / form submit, true proceeds.


๐ŸŒ I18n

Locales en and fr ship with the gem. To copy them into your app for customization:

$ bin/rails g swal_rails:locales

Generated keys:

en:
  swal_rails:
    confirm_button_text: "OK"
    cancel_button_text:  "Cancel"
    deny_button_text:    "No"
    close_button_aria_label: "Close this dialog"

The current I18n.locale is read on every request and injected into the client payload โ€” change languages, button labels follow.


โ™ฟ Accessibility

  • Reduced motion โ€” when prefers-reduced-motion: reduce is set, animations are disabled (showClass/hideClass emptied).
  • Focus trap & ARIA โ€” SweetAlert2's built-in focus management and ARIA roles are preserved; returnFocus: true brings focus back to the trigger.
  • Translatable labels โ€” all button / aria labels go through I18n.

๐Ÿ”’ Security & CSP

XSS safety

All strings flowing through the Ruby helpers are hardened against the usual breakout sequences:

  • swal_tag runs the serialized options through ERB::Util.json_escape, which neutralizes </script>, <!--, U+2028 and U+2029 before they reach the inline <script> body.
  • swal_config_meta_tag and swal_flash_meta_tag emit <meta> attributes, which Rails HTML-escapes automatically; the JS runtime feeds messages to SweetAlert2's text: option (not html:), so flash payloads are rendered as text even if they contain HTML.

โš ๏ธ Hash-form overrides bypass the text: safety net. When you pass a Hash to flash[key], data-turbo-confirm, data-swal-confirm, or data-swal-options, its keys flow straight into Swal.fire. Using html:, iconHtml:, or footer: with untrusted input is an XSS โ€” SweetAlert2 renders those as raw HTML by design. Rule of thumb: if the value is user-controlled, keep the String form (or the text: key).

Content Security Policy

Meta tags carry no script and need no nonce. For the inline helper:

<%= swal_tag({ title: "Saved" }, nonce: true) %>

When ActionView's CSP helper is available, Rails substitutes the per-request nonce; otherwise nonce: true is silently dropped so the tag stays valid on apps without a configured CSP.

SweetAlert2 + strict style-src โ€” SA2 injects styles via JavaScript. Under style-src 'self' with no 'unsafe-inline', the popups won't be styled. Either ship SA2's CSS via your normal stylesheet (the gem vendors sweetalert2.css) or allow a style nonce for the inserted tags.


๐ŸŽญ Themes

The six official SweetAlert2 themes are vendored alongside the default stylesheet โ€” pick one, load it in your layout, and set the data-swal2-theme attribute to activate it.

Theme File
Bootstrap 4 sweetalert2/themes/bootstrap-4.css
Bootstrap 5 sweetalert2/themes/bootstrap-5.css
Borderless sweetalert2/themes/borderless.css
Bulma sweetalert2/themes/bulma.css
Material UI sweetalert2/themes/material-ui.css
Minimal sweetalert2/themes/minimal.css

Importmap / jsbundling (apps that load their own CSS)

<%# app/views/layouts/application.html.erb %>
<%= stylesheet_link_tag "sweetalert2/themes/bootstrap-5" %>
<body data-swal2-theme="bootstrap-5">

Sprockets

/* app/assets/stylesheets/application.css */
*= require sweetalert2/themes/bootstrap-5

bootstrap-5 and material-ui ship both a light and a dark variant baked into the same file โ€” set data-swal2-theme="bootstrap-5-dark" or "material-ui-dark" to opt into dark mode.

OS-driven dark mode? Hook the attribute to prefers-color-scheme with a one-liner:

document.body.dataset.swal2Theme =
  matchMedia("(prefers-color-scheme: dark)").matches
    ? "bootstrap-5-dark" : "bootstrap-5"

๐ŸŽจ Asset pipelines

swal_rails adapts to whichever pipeline you use โ€” the generator picks the right template automatically.

โ”Œโ”€ Importmap (Rails 7+ default) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   config/importmap.rb                                                โ”‚
โ”‚     pin "sweetalert2", to: "sweetalert2.js"                          โ”‚
โ”‚     pin "swal_rails",  to: "swal_rails.js"                           โ”‚
โ”‚   app/javascript/application.js                                      โ”‚
โ”‚     import "swal_rails"                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€ jsbundling (esbuild / vite / rollup) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   package.json  โ†’  "sweetalert2": "^11"  (your bundler resolves it)  โ”‚
โ”‚   app/javascript/application.js                                      โ”‚
โ”‚     import "swal_rails"                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€ Sprockets (legacy) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   app/assets/javascripts/application.js                              โ”‚
โ”‚     //= require sweetalert2                                          โ”‚
โ”‚     //= require swal_rails                                           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿงช Development

$ git clone https://github.com/Metalzoid/swal_rails.git
$ cd swal_rails
$ bundle install
$ bundle exec rspec         # 47 examples, real headless Chromium (Cuprite)
$ bundle exec rubocop       # style

Test against a specific Rails version:

$ bundle exec appraisal install
$ BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rspec
$ BUNDLE_GEMFILE=gemfiles/rails_8_1_sprockets.gemfile bundle exec rspec

Repo layout

swal_rails/
โ”œโ”€โ”€ app/                           # Engine: helpers, views, assets
โ”œโ”€โ”€ lib/
โ”‚   โ”œโ”€โ”€ swal_rails.rb              # entrypoint + Engine + Railtie
โ”‚   โ”œโ”€โ”€ swal_rails/configuration.rb
โ”‚   โ””โ”€โ”€ generators/
โ”‚       โ”œโ”€โ”€ install/               # bin/rails g swal_rails:install
โ”‚       โ””โ”€โ”€ locales/               # bin/rails g swal_rails:locales
โ”œโ”€โ”€ vendor/javascript/sweetalert2/ # pinned SA2 v11.26.24 (MIT)
โ”œโ”€โ”€ spec/
โ”‚   โ””โ”€โ”€ dummy/                     # minimal Rails app for system tests
โ”œโ”€โ”€ Appraisals                     # Rails 7.2 โ†’ 8.1 + sprockets variant
โ””โ”€โ”€ gemfiles/                      # per-version lockfiles (generated)

๐Ÿค Contributing

Bug reports and pull requests welcome on GitHub at https://github.com/Metalzoid/swal_rails.

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Make sure tests and RuboCop pass (bundle exec rspec && bundle exec rubocop)
  4. Push (git push origin my-new-feature)
  5. Open a Pull Request

๐Ÿ™ Credits & license

This gem bundles SweetAlert2 by Tristan Edwards, Limon Monte and contributors, distributed under the MIT License. The full license is included at vendor/javascript/sweetalert2/LICENSE.

swal_rails itself is released under the MIT License.

Built with ๐Ÿฌ by [Metalzoid](https://github.com/Metalzoid)