View as Markdown

Closeable

A Stimulus behavior that removes its element from the page when dismissed, with optional delayed and automatic closing and an animated countdown bar.

100%
Loading...
<div data-controller="closeable" class="">
    <div class="">
        <p class="">Heads up!</p>
        <p class="">This message can be dismissed.</p>
    </div>
    <button type="button" data-action="click->closeable#close" aria-label="Dismiss" class="">
        <span aria-hidden="true" class="">&times;</span>
    </button>
</div>

Installation

php bin/console ux:install closeable --kit common

Copy the following file(s) into your app:

assets/controllers/closeable_controller.js
import { Controller } from '@hotwired/stimulus';

/**
 * @value  autoClose  Delay in milliseconds after which the element removes itself once connected.
 * @target timerbar   Element whose width animates down to 0 over the close delay to visualize the countdown. It is hidden until a delayed or automatic close starts.
 * @action close      Removes the element. Accepts a `data-closeable-delay-param` (milliseconds) to defer the removal.
 * @action cancel     Cancels a pending delayed or automatic close.
 */
export default class extends Controller {
    static targets = ['timerbar'];
    static values = {
        autoClose: Number, // auto close delay in ms
    };

    #timeout;

    connect() {
        if (this.hasTimerbarTarget) {
            this.timerbarTarget.hidden = true;
        }

        if (this.hasAutoCloseValue) {
            this.#remove(this.autoCloseValue);
        }
    }

    close({ params }) {
        this.#remove(Math.max(0, params.delay || 0));
    }

    cancel() {
        clearTimeout(this.#timeout);

        if (this.hasTimerbarTarget) {
            this.timerbarTarget.hidden = true;
        }
    }

    #remove(delay = 0) {
        // Cancel any pending close so repeated triggers can't race an earlier timeout.
        clearTimeout(this.#timeout);

        if (this.hasTimerbarTarget && delay) {
            const timerbar = this.timerbarTarget;
            timerbar.hidden = false;
            // Snap to full width without animating, then transition down to 0 over the delay.
            timerbar.style.transitionDuration = '0ms';
            timerbar.style.width = '100%';
            setTimeout(() => {
                timerbar.style.transitionDuration = `${delay}ms`;
                timerbar.style.width = '0';
            }, 10);
        }

        this.#timeout = setTimeout(() => this.element.remove(), delay);
    }
}

Usage

Add data-controller="closeable" to the element you want to remove, then trigger closeable#close from a child element:

<div data-controller="closeable">
    <button type="button" data-action="click->closeable#close">Dismiss</button>
</div>

Examples

Delayed Close

Set a data-closeable-delay-param (in milliseconds) on the close action to defer the removal. Add a timerbar target to visualize the countdown.

100%
Loading...
<div data-controller="closeable" class="">
    <div class="">
        <p class="">Saved!</p>
        <p class="">Dismiss to close after a short delay.</p>
    </div>
    <button type="button" data-action="click->closeable#close" data-closeable-delay-param="3000" aria-label="Dismiss" class="">
        <span aria-hidden="true" class="">&times;</span>
    </button>
    <div data-closeable-target="timerbar" class=""></div>
</div>

Auto Close

Set data-closeable-auto-close-value (in milliseconds) to remove the element automatically once it connects. The timerbar target animates down over the same duration.

100%
Loading...
<div data-controller="closeable" data-closeable-auto-close-value="5000" class="">
    <div class="">
        <p class="">Copied to clipboard</p>
        <p class="">This message closes on its own.</p>
    </div>
    <button type="button" data-action="click->closeable#close" aria-label="Dismiss" class="">
        <span aria-hidden="true" class="">&times;</span>
    </button>
    <div data-closeable-target="timerbar" class=""></div>
</div>

Cancel Auto Close

Call closeable#cancel to stop a pending close. Here, hovering the message cancels the automatic close so the user has time to read it.

100%
Loading...
<div data-controller="closeable" data-closeable-auto-close-value="5000" data-action="mouseenter->closeable#cancel" class="">
    <div class="">
        <p class="">Hover to keep me</p>
        <p class="">Move your pointer over this message to cancel the automatic close.</p>
    </div>
    <button type="button" data-action="click->closeable#close" aria-label="Dismiss" class="">
        <span aria-hidden="true" class="">&times;</span>
    </button>
    <div data-closeable-target="timerbar" class=""></div>
</div>

API Reference

data-controller="closeable"

Value Type Default
data-closeable-auto-close-value 
Number -
Target Description
timerbar Element whose width animates down to 0 over the close delay to visualize the countdown. It is hidden until a delayed or automatic close starts.
Action Description
close Removes the element. Accepts a data-closeable-delay-param (milliseconds) to defer the removal.
cancel Cancels a pending delayed or automatic close.