Project

swal_rails

0.0
The project is in a healthy, maintained state
swal_rails integrates SweetAlert2 v11 into Rails 7+ applications with first-class support for importmap, jsbundling, and sprockets. Includes auto-wired flash messages, Turbo confirm replacement, a Stimulus controller, Ruby view helpers, and full I18n.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 7.2
>= 2.6
 Project Readme

๐Ÿฌ 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 Gem Version Ruby Rails SweetAlert2 License


๐Ÿ“– Table of contents

  • Why swal_rails?
  • Features
  • Compatibility
  • Installation
  • Quick start
  • Configuration
  • Usage
    • Flash messages
    • Turbo confirmations
    • data-swal-confirm attribute
    • Multi-step confirmations
    • Stimulus controller
    • Ruby view helpers
    • Programmatic JS
  • Reference
    • SwalRails.configure
    • View helpers
    • Data attributes
    • Stimulus controller reference
    • JS runtime
    • Generators
    • Flash value shapes
    • Chain semantics
  • I18n
  • Accessibility
  • Security & CSP
  • Themes
  • Asset pipelines
  • Development
  • Contributing
  • Credits & license

๐Ÿค” 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] / flash[:alert] โ†’ toast, stackable, 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: true, position: "top-end", timer: 4000 }
  config.flash_map[:error]   = { icon: "error",   toast: true, position: "top-end", timer: 4000 }
  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 }

  # โ”€โ”€โ”€ Multi-entry flash playback โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  config.flash_array_mode  = :sequential  # :sequential | :stacked
  config.flash_stack_delay = 500          # ms between stacked toasts

  # โ”€โ”€โ”€ 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"   # โ†’ success toast top-right
flash[:alert]  = "Could not save"    # โ†’ error toast top-right (since 0.3.1.beta2)

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

Multi-entry playback: sequential vs stacked

When more than one flash entry is set in a single request โ€” either through an array of messages under one key, or multiple distinct keys โ€” the runtime picks one of two playback modes (configurable via config.flash_array_mode):

Mode Behavior
:sequential (default) Each Swal fires only after the previous one closes โ€” chained via Promise callbacks. Predictable but slow on long lists.
:stacked All entries fire in parallel into a fixed top-right container, stacking vertically. Each appears flash_stack_delay ms after the previous (default 500ms), then runs its own timer independently. Any toast: false entry is forced to toast in this mode.

swal_flash helper (per-request override)

For one-off overrides without touching the global config, use swal_flash from controllers or views:

# Pile up validation errors as a stack of toasts with a quicker 300ms cadence
swal_flash :alert, @post.errors.full_messages, mode: :stacked, delay: 300

# Same mode but over flash.now (for `render`, not `redirect_to`)
swal_flash :alert, "Form incomplete", now: true

# Extra SA2 options are merged into every entry
swal_flash :notice, "Deployed!", icon: "rocket", timer: 5000

Signature: swal_flash(key, messages, mode: nil, delay: nil, now: false, **options). mode: and delay: are stored on the flash entry as reserved _arrayMode / _stackDelay meta-keys, extracted by the JS runtime before the options are handed to Swal.fire โ€” they never leak into SA2.

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.
flash_array_mode Symbol :sequential How multi-entry flash payloads are played: :sequential (one at a time, waits for close) or :stacked (all in parallel, stacked top-right). Validated.
flash_stack_delay Integer 500 Milliseconds between each toast's appearance in :stacked mode.
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: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false },
  error:   { icon: "error",   toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false },
  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 }
}

๐Ÿ’ก Prefer a modal for errors? Override in your initializer: config.flash_map[:alert] = { icon: "error", toast: 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,
  flashArrayMode:       Symbol,  # :sequential | :stacked
  flashStackDelay:      Integer, # ms
  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