StimulusJS, born at Basecamp, was officially released around a year ago - and I kind of ignored it’s existence as I simply didn’t need it anywhere (I worked on React-based project for a while). But right now I’m working with an app that relies heavily on backend-rendering and Rails itself - in such scenario dropping the whole ecosystem of any javascript framework with client-side rendering makes no sense business-wise. But gluing random js snippets can get you far until some point.

I think the biggest advantage of stimulus is that it gives you some sane structure. It integrates nicely with webpack/er and introduces idea of controllers. So let’s take a practical example of what we can do with it.

PhotoSwipe is pretty neat vanilla-js gallery library. But it requires some time to set up, and docs to be honest looks pretty intimidating. So let’s break it down into the most simple example, so we can quickly see some results.

Add required HTML markup

Note: I assume you already added photoswipe, its styles and you have stimulusjs ready to go.

We need to explicitly add required markup somewhere in our document, so let’s slap this partial just before closing </body> tag. You will notice we can already incorporate I18n for translating captions/navigation - which is pretty great as you can stick with ruby/rails for that.

<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
  <div class="pswp__bg"></div>
  <div class="pswp__scroll-wrap">
    <div class="pswp__container">
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
    <div class="pswp__ui pswp__ui--hidden">
      <div class="pswp__top-bar">
        <div class="pswp__counter"></div>
        <!-- You can replace those t() calls with static text for the sake of example -->
        <button class="pswp__button pswp__button--close" title="<%= t('gallery.close') %>"></button>
        <button class="pswp__button pswp__button--share" title="<%= t('gallery.share') %>"></button>
        <button class="pswp__button pswp__button--fs" title="<%= t('gallery.fullscreen') %>"></button>
        <button class="pswp__button pswp__button--zoom" title="<%= t('gallery.zoom') %>"></button>
        <div class="pswp__preloader">
          <div class="pswp__preloader__icn">
            <div class="pswp__preloader__cut">
              <div class="pswp__preloader__donut"></div>

      <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
        <div class="pswp__share-tooltip"></div>

      <button class="pswp__button pswp__button--arrow--left" title="<%= t('gallery.previous') %>">

      <button class="pswp__button pswp__button--arrow--right" title="<%= t('') %>">

      <div class="pswp__caption">
        <div class="pswp__caption__center"></div>

I think the most common scenario is that you already render some markup on the backend and you want to initialize a gallery based on that, so at this point we will utilize stimulus for that. Our simple markup might look something like that:

<!-- we point this element to gallery controller -->
<div data-controller="gallery">
  <!-- and within the gallery we want to render our <image> collection/record/whatever -->
  <!-- let's connect click action <a> to onImageClick method in our controller -->
  <!-- and specify target that we can nicely access in controller -->
  <%= link_to image.big_file_destination, title: image.title, data: { action: 'gallery#onImageClick', target: 'gallery.picture' } do %>
    <%= image_tag image.small_thumb %>
  <% end %>
// gallery_controller.js
import {Controller} from 'stimulus'
import * as PhotoSwipe from 'photoswipe'
import * as PhotoSwipeUI_Default from 'photoswipe/dist/photoswipe-ui-default'

export default class extends Controller {
  static targets = ['picture']

  onImageClick(event) {

    // as our gallery markup lives outside of our controller
    // unfortunately we need to query for it, for the simplicity of example
    // let's assume we have single gallery controller in the app and we can call
    // query selector directly by it's class and we don't need to extract it into
    // configurable data-attribute
    const galleryWrapper = document.querySelector('.pswp')

    var options = {
      // we don't want browser history for or example for the sake of simplicity
      history: false,
      // and I'm assuming we have unique links in each gallery
      index: this.items.findIndex(item => item.src === event.currentTarget.getAttribute('href'))

    var gallery = new PhotoSwipe(galleryWrapper, PhotoSwipeUI_Default, this.items, options)

    // PhotoSwipe requires width and height do be declared up-front
    // let's work around that limitation, references:
    gallery.listen('beforeChange', function() {
      const src = gallery.currItem.src

      const image = new Image()
      image.src = src

      image.onload = () => {
        gallery.currItem.w = image.width
        gallery.currItem.h = image.height



  get items() {
    return {
      return {
        src: item.getAttribute('href'),
        title: item.getAttribute('title'),
        w: 0,
        h: 0

So why this is better than simply dropping some js code on DOMContentLoaded somewhere within your app? As said before - it gives you a nice structure, our gallery logic is nicely encapsulated within javascript controller. We had to add a minimal amount of extra html markup to our code. Let’s say we would like to handle async removal of image elements - combining stimulus with rails ujs events (binding action like ajax:success->gallery#imageRemoved) is pretty powerful and effortless.

I have been using stimulus for over 2 months now and if you’re not planning on doing any intense client-side rendering / very dynamic UI I think it’s a pretty solid tiny framework that is worth checking out.

Update: Dec 18, 2019

As this post got some traction let’s also briefly discuss how to properly attach stylesheets to your wepack bundle - as I received few questions regarding that matter as well.

I assume you already have some sort of application pack file that is compiled by webpack. Simply put there:

// packs/application.scss

// Specifying pswp__assets-path might not be needed - it depends on your webpack config
// Please read:
// for references regarding url resolver
$pswp__assets-path: '~photoswipe/src/css/default-skin/';
@import '~photoswipe/src/css/main';
@import '~photoswipe/src/css/default-skin/default-skin';

And that should do it!

Update: May 21, 2020

If you’re wondering how to add video support to PhotoSwipe wonder no more ;).