<h1>Spoiler</h1>
<details>
  <summary>Citizen Kane Spoilers Ahead!</summary>
  Rosebud is the sled
</details>

<h1>Inline Spoiler</h1>
<h2>Background</  h2>
<p tabindex="0" class="spoiler-1">Rosebud is the sled 😀</p>
<h2>Brightness</h2>
<p tabindex="0" class="spoiler-2">Rosebud is the sled 😀</p>
<h2>Blur</h2>
<p tabindex="0" class="spoiler-3">Rosebud is the sled 😀</p>
<h2>Toggle</h2>
<input type="checkbox" id="spoiler-4" />
<label class="spoiler-4" tabindex="0" for="spoiler-4"> Rosebud is the sled 😀</label>
<h2>Custom Element</h2>
<wc-spoiler>Rosebud is the sled 😀</wc-spoiler>
<wc-spoiler label="Citizen Kane Spoilers">Rosebud is the sled 😀</wc-spoiler>
.spoiler-1 {
  background: black;
  display: inline-block;
}
.spoiler-1:active,
.spoiler-1:focus {
  background: transparent;
}

.spoiler-2 {
  filter: brightness(0%);
  background: white;
  display: inline-block;
}
.spoiler-2:active,
.spoiler-2:focus {
  filter: none;
  background: transparent;
}

.spoiler-3 {
  filter: blur(10px);
  display: inline-block;
}
.spoiler-3:active,
.spoiler-3:focus {
  filter: none;
}

.spoiler-4 {
  filter: brightness(0%);
  background: white;
}
input[type="checkbox"]#spoiler-4{
  clip: rect(0 0 0 0);
  height: 0px;
  width: 0px;
  padding: 0px;
  margin: 0px;
}
.spoiler-4:has(input:checked),
input:checked + .spoiler-4{
  filter: none;
  background: white;
}
.spoiler-4:focus {
  outline: 1px solid green;
}
class WcSpoiler extends HTMLElement {
	#isShown = false;
	bind(element) {
		this.render = this.render.bind(element);
		this.cacheDom = this.cacheDom.bind(element);
		this.attachEvents = this.attachEvents.bind(element);
	}

	connectedCallback() {
		this.render();
		this.cacheDom();
		this.attachEvents();
	}

	render() {
		this.attachShadow({ mode: "open" });
    const label = this.getAttribute("label")
      ? this.getAttribute("label")
      : "Toggle to read spoiler";
    
		this.shadowRoot.innerHTML = `
			<style>
			:host { position: relative; display: inline-block; }
			#label-button {
				width: 100%;
				height: 100%;
				position: absolute;
				top: 0px;
				left: 0px;
				background-color: black;
				border: none;
			}
			#label-button[aria-pressed="true"] {
				background-color: transparent;
			}
			#button-text, #live-area {
				clip: rect(0 0 0 0);
				height: 0px;
				width: 0px;
				padding: 0px;
				margin: 0px;
				overflow: hidden;
			}
			</style>
			<slot aria-hidden="true"></slot>
			<div id="live-area" aria-live="polite"></div>
			<button id="label-button" aria-role="switch"><div id="button-text">${label}</div></button>
			`;
	}
	cacheDom() {
		this.dom = {
			labelButton: this.shadowRoot.querySelector("#label-button"),
			buttonText: this.shadowRoot.querySelector("#button-text"),
			liveArea: this.shadowRoot.querySelector("#live-area"),
			slot: this.shadowRoot.querySelector("slot")
		};
	}
  attributeChangedCallback(name, oldValue, newValue) {
     if (oldValue !== newValue) {
      this[name] = newValue;
     }
  }
	attachEvents() {
		this.dom.labelButton.addEventListener("click", () => {
			this.#isShown = !this.#isShown;
      this.dom.slot.ariaHidden = this.#isShown ? "false" : "true";
			this.dom.labelButton.ariaPressed = this.#isShown ? "true" : "false";
			this.dom.liveArea.textContent = this.#isShown ? this.textContent : "";
		});
	}
  set label(val){
    if(this.dom?.buttonText && val.trim()){
      this.dom.buttonText = val;
    }
  }
}

customElements.define("wc-spoiler", WcSpoiler);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.