Component Library 2.3.12 (configured with Dummy App)

Previews

      • Button
      • Hyper Link
      • Icon
        • Email
        • Radio
        • Telephone
        • Text
        • Textarea
      • Paginator
      • Article
        • Default
        • Hero Only
        • Multi Panel
      • Carousel
      • Fault Tolerance
      • Footer
      • Header
        • Background Hero With Breadcrumbs
        • Default
      • Pattern
        • Default
        • Error
        • Filled
        • No Legend
      • Image
      • Media Card
      • Text Card
  • Style Guide

No matching results.

Pages

  • Developer Guide
  • Overview
  • Colours
  • Typography
  • Spacing
  • New Components
  • Interactive Behaviour
  • Component Variants
  • CSS Utilities

No matching results.

Interactive Behaviour

Interactive Behaviour

Interactive behaviour is handled in two layers: CSS for style responses to state, Stimulus controllers for anything requiring JS. Controllers live in app/javascript/gll_component_library/controllers/.

Controllers are UX sprinkles — DOM interactions, class toggles, measurements. Not application logic.

CSS First

Reach for CSS before JS. Hover states, focus styles, transitions, and class-based show/hide need no controller:

.navigation__item:hover { background-color: var(--nav-item-bg-hover); }
&:has(+ .open) { transform: rotate(180deg); }
.submenu {
max-height: 0;
overflow: hidden;
&.open { max-height: 2000px; }
}

Use a controller only when the effect requires measuring the DOM, reading runtime values, or responding to events CSS can't handle.

State Classes

Controllers toggle classes on elements; CSS responds to them:

Class Applied to Effect
.open Submenus, nav items, panels Expanded/visible
.rotate Arrow/chevron icons Rotated state
.loaded Material Symbols icons Icon font has loaded
.disabled Buttons Non-interactive
this.targetTarget.classList.toggle('open')
this.arrowbuttonTarget.classList.toggle('rotate')

Viewport Breakpoint

Use 1024 in JS — matches @include mobiletablet:

if (window.innerWidth < 1024) { ... }

Passing Measurements to CSS

Pass JS measurements to CSS via a custom property on the controller element:

setWidth() {
const width = this.logoTarget.offsetWidth
this.element.style.setProperty('--width', `${width}px`)
}

Reading Configuration

Pass configuration via data-* attributes on the controller element; read in connect():

connect() {
this.itemsPerView = this.element.getAttribute('data-items-per-view')
this.pagination = this.element.getAttribute('data-pagination') === 'true'
}

Document-Level Listeners

Add in connect(), remove in disconnect():

connect() {
this.handleClick = this.handleClick.bind(this)
document.addEventListener('click', this.handleClick, true)
}
disconnect() {
document.removeEventListener('click', this.handleClick, true)
}

Controller Conventions

Filename snake_case → controller name kebab-case: open_toggle_controller.js → data-controller="open-toggle".

Standard Structure

import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="my-thing"
export default class extends Controller {
static targets = ["target"] // only if targets are used
connect() { } // one-time DOM setup
disconnect() { } // required if document/window listeners added
myAction() { } // called via data-action="event->my-thing#myAction"
}

Targets

static targets = ["logo"]
connect() {
if (this.hasLogoTarget) { ... } // guard before accessing
}

Access: this.nameTarget (first match), this.nameTargets (all), this.hasNameTarget (boolean).

Style

  • const / let — no var
  • Arrow functions: () => this.method()
  • No semicolons
  • No TypeScript
Adding Components Component Structure & Variants