๐ Table of contents
- Why swal_rails?
- Features
- Compatibility
- Installation
- Quick start
- Configuration
- Usage
- Reference
- 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]โ 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/frlocales 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)
sweetalert2andswal_rails - ๐ injects
<%= swal_rails_meta_tags %>intoapp/views/layouts/application.html.erb - ๐ loads the
en/frlocales
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. = {
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. # ["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 bareimport 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 callwindow.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 exposescontent_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_tagas<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, callwindow.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:
config/importmap.rbpresent โimportmappackage.jsonpresent โjsbundling- fallback โ
sprockets
Per-mode side effects:
- importmap: appends
pin "sweetalert2", to: "sweetalert2.esm.all.js"andpin "swal_rails", to: "swal_rails/index.js"toconfig/importmap.rb; appendsimport "swal_rails"toapp/javascript/application.js. - jsbundling: runs
yarn add sweetalert2@<pinned>ornpm install sweetalert2@<pinned>(based on the lockfile present); appendsimport "swal_rails"toapp/javascript/application.js. - sprockets: appends
//= link sweetalert2.jsand//= link sweetalert2.csstoapp/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. Usinghtml:,iconHtml:, orfooter: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
stepsinput resolvestrueimmediately. - A chain resolves
trueiff 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/onDeniedkeys are stripped before being passed toSwal.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: reduceis set, animations are disabled (showClass/hideClassemptied). - Focus trap & ARIA โ SweetAlert2's built-in focus management and ARIA
roles are preserved;
returnFocus: truebrings 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_tagruns the serialized options throughERB::Util.json_escape, which neutralizes</script>,<!--, U+2028 and U+2029 before they reach the inline<script>body.swal_config_meta_tagandswal_flash_meta_tagemit<meta>attributes, which Rails HTML-escapes automatically; the JS runtime feeds messages to SweetAlert2'stext:option (nothtml:), 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 toflash[key],data-turbo-confirm,data-swal-confirm, ordata-swal-options, its keys flow straight intoSwal.fire. Usinghtml:,iconHtml:, orfooter: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 thetext: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. Understyle-src 'self'with no'unsafe-inline', the popups won't be styled. Either ship SA2's CSS via your normal stylesheet (the gem vendorssweetalert2.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-schemewith 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.
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Make sure tests and RuboCop pass (
bundle exec rspec && bundle exec rubocop) - Push (
git push origin my-new-feature) - 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.