---
title: Party mode! Hotwired Stimulus 101
url: https://calvin.my/posts/party-mode-hotwired-stimulus-101
published: 2024-07-15
updated: 2026-09-17
category: Development
tags:
- Hotwired
- Stimulus
- Javascript
summary: 'This tutorial uses a website “Party Mode” to introduce Stimulus concepts: controllers, actions, targets, and values. A keyboard shortcut toggles fullscreen display, adjusts header height, creates animated colored bubbles, applies a multicolor title effect, and starts or stops background music. The example also demonstrates integrating browser APIs, CSS and Tailwind styling, DOM references, controller lifecycle state, and asset-based configuration in a compact Stimulus implementation.'
---

# Party mode! Hotwired Stimulus 101

[Stimulus](https://stimulus.hotwired.dev/) is a relatively minimalistic JavaScript framework which usually consists of 3 steps:

- When A happens (Actions)
- Performs B (Controllers)
- Reflects the result on C (Targets)

* * *

To explore Stimulus's features, I added a Party Mode on this website. The specifications:

- It can be switched on and off via a keyboard shortcut key.
- When it is switched on, it does the following:
  - Show the browser in full-screen
  - Expand the header section to 100% height
  - Have some colorful bubbles moving from the bottom to the top of the screen
  - Change the page title into a flashing neon light
  - Play a rock music!

You can experience the final result by pressing Ctrl+B on this page before reading further about it.

* * *

## Creating the controller

1. First I create a controller called party\_controller.js

   ```javascript
   import {Controller} from "@hotwired/stimulus"

   export default class extends Controller {

   }
   ```

2. Next I want to switch on/off via a shortcut key

   ```html
   <header data-controller="party" data-action="keydown.ctrl+b@document->party#partyMode">
   </header>
   ```

   This connects the header section with our party controller. It also defines an action - when Ctrl+B is pressed anywhere it invokes the "partyMode" method.

   ```javascript
   import {Controller} from "@hotwired/stimulus"

   export default class extends Controller {
       initialize() {
           this.isRunning = false;
       }

       partyMode() {
           this.isRunning = !this.isRunning;
       }
   }
   ```

   I want to know the state of my party mode. This is kept in a variable called "isRunning". The variable has an initial value set in the initialize method, which is a Stimulus controller life-cycle callback. Other types of callbacks can be found [here](https://stimulus.hotwired.dev/reference/lifecycle-callbacks).
* * *
## Make it full-screen!

1. My first requirement is to turn the current tab into fullscreen when party mode is on and cancel fullscreen when it is off.

   ```javascript
   import {Controller} from "@hotwired/stimulus"

   export default class extends Controller {
       initialize() {
           this.isRunning = false;
       }

       partyMode() {
           this.toggleFullscreen();
           this.isRunning = !this.isRunning;
       }

       toggleFullscreen() {
           if (this.isRunning) {
               if (document.fullscreenElement) {
                   document.exitFullscreen();
               }
           } else {
               if (!document.fullscreenElement) {
                   document.documentElement.requestFullscreen();
               }
           }
       }
   }
   ```

   This works well, there is no issue calling browser API (e.g. requestFullscreen) from the Stimulus controller.

2. Next, I want to expand the header section to match the screen height when party mode is on and revert to the original height when it is off.&nbsp;

   I will implement this by switching between 2 tailwind classes - h-96 (original height) and h-screen (screen height).

   ```javascript
   static targets = [ "container" ]

   partyMode() {
       this.toggleFullscreen();
       this.isRunning = !this.isRunning;
   }

   toggleFullscreen() {
       if (this.isRunning) {
           this.containerTarget.classList.remove('h-screen');
           this.containerTarget.classList.add('h-96');
           if (document.fullscreenElement) {
               document.exitFullscreen();
           }
       } else {
           this.containerTarget.classList.remove('h-96');
           this.containerTarget.classList.add('h-screen');
           if (!document.fullscreenElement) {
               document.documentElement.requestFullscreen();
           }
       }
   }
   ```

   Instead of accessing the DOM via getElementById, Stimulus provides a "Target" feature to reference the DOM.

   ```html
   <header data-controller="party" data-party-target="container" data-action="keydown.ctrl+b@document->party#partyMode">

   </header>
   ```
* * *
## Create some bubbles

1. My next specification is to create some bubbles that move from the bottom to the top of the screen. These bubbles should appear in a random x position at the bottom of the screen, have a random color, and move upward at a random speed.

   In the Stimulus controller, you can define a constant value outside of the controller class, and use it in your controller methods.

   ```javascript
   import {Controller} from "@hotwired/stimulus"

   const MAX_BUBBLES = 35;
   const BUBBLE_MIN_SIZE = 10;
   const BUBBLE_SIZE_VAR = 20;
   const BUBBLE_MIN_SPEED = 2.0;
   const BUBBLE_SPEED_VAR = 1.0;

   export default class extends Controller {
       static targets = [ "container", "section" ]

       initialize() {
           this.isRunning = false;
           this.bubbles = [];
       }

       partyMode() {
           this.toggleFullscreen();
           this.toggleBubbles();
           this.isRunning = !this.isRunning;
       }

       toggleBubbles() {
           if (this.isRunning) {
               this.cleanBubbles();
           } else {
               this.initBubbles();
           }
       }

       initBubbles() {
           for (let i = 0; i < MAX_BUBBLES; i++) {
               this.createBubble();
           }
           this.animateBubbles();
       }

       cleanBubbles() {
           this.bubbles.forEach((bubble) => {
               this.containerTarget.removeChild(bubble.element);
           });
           this.bubbles.length = 0;
       }

       createBubble() {
           if (this.bubbles.length >= MAX_BUBBLES) return;

           const bubble = document.createElement('div');
           bubble.className = 'absolute rounded-full opacity-50';

           const size = Math.random() * BUBBLE_SIZE_VAR + BUBBLE_MIN_SIZE;
           const x = Math.random() * this.sectionTarget.offsetWidth;
           const y = Math.random() * this.sectionTarget.offsetHeight;

           bubble.style.width = `${size}px`;
           bubble.style.height = `${size}px`;
           bubble.style.left = `${x}px`;
           bubble.style.top = `${y}px`;

           bubble.classList.add(COLORS[Math.floor(Math.random() * COLORS.length)]);

           this.containerTarget.appendChild(bubble);
           this.bubbles.push({ element: bubble, x, y, speed: Math.random() * BUBBLE_SPEED_VAR + BUBBLE_MIN_SPEED });
       }

       animateBubbles() {
           this.bubbles.forEach((bubble) => {
               bubble.y -= bubble.speed;
               if (bubble.y + parseFloat(bubble.element.style.height) < 0) {
                   bubble.y = this.sectionTarget.offsetHeight;
               }
               bubble.element.style.top = `${bubble.y}px`;
           });

           requestAnimationFrame(() => this.animateBubbles());
       }
   }
   ```
* * *
## Animate the title with random colors

1. To animate the title, I need to first define it as a target. Let's call it "neon".

   ```html
   <h1 class="text-4xl font-bold" data-party-target="neon">Calvin's Dev Logs</h1>
   ```

2. Next, a list of colors is needed. It is defined using CSS keyframes.

   ```css
   @keyframes rainbowTextEffect {
       0% { color: white; }
       11% { color: red; }
       22% { color: orange; }
       33% { color: yellow; }
       44% { color: green; }
       55% { color: lightskyblue; }
       66% { color: plum; }
       77% { color: violet; }
       88% { color: darkorange }
       100% { color: white; }
   }
   ```

3. Then I create a method to toggle between the plain text version and the colored version.

   ```javascript
   const NEON_DELAY = 3;

   ...
   ...

   partyMode() {
       this.toggleFullscreen();
       this.toggleBubbles();
       this.toggleNeon();
       this.isRunning = !this.isRunning;
   }

   toggleNeon() {
       if (this.isRunning) {
           this.neonTarget.textContent = this.orignalNeonContent;
       } else {
           this.orignalNeonContent = this.neonTarget.textContent;
           this.neonTarget.innerHTML = Array.from(this.orignalNeonContent).map(char => {
               const delay = Math.random() * NEON_DELAY;
               return `<span style="animation: rainbowTextEffect 3s infinite alternate; animation-delay: ${delay}s">${char}</span>`;
           }).join('');
       }
   }
   ```

   With a random animation-delay, each character starts animating the keyframes at a different time.
* * *
## Play Music

1. My last requirement is to play some background music when the party mode is on.

   I create a piece of random rock music using Google Music FX.

   ![](https://camy-pub.s3.ap-southeast-1.amazonaws.com/0ffc35b5-f24f-4773-9779-5f263747e90d.png)

2. The music file is stored in the Rails assets directory. I decided to print the URL of the file into a Stimulus value.

   ```html
   <header data-controller="party" 
     data-party-target="container" 
     data-action="keydown.ctrl+b@document->party#partyMode" 
     data-party-music-value="<%= audio_url('music_fx.wav') %>">

   </header>
   ```

3. This can be read from the Stimulus controller via the value feature.

   ```javascript
   static values = {
       music: String
   }

   initialize() {
       this.isRunning = false;
       this.audio = new Audio(this.musicValue);
   }

   partyMode() {
       this.toggleFullscreen();
       this.toggleBubbles();
       this.toggleNeon();
       this.toggleMusic();
       this.isRunning = !this.isRunning;
   }

   toggleMusic() {
       if (this.isRunning) {
           this.audio.pause();
           this.audio.currentTime = 0;
           this.audio.removeEventListener('ended', () => {});
       } else {
           this.audio.addEventListener('ended', () => {
               this.audio.play();
           });
           this.audio.play();
       }
   }
   ```

   To play the music indefinitely, I just listen to the ended event and replay it again.

* * *

## Summary

The demo above shows the controller, action, target, and value features in Stimulus. It also shows interaction with Browser API and working with Tailwind classes.

It is a decent and easy-to-use JavaScript framework.&nbsp;
