Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Andy Bell's progressive disclosure (web) component implemented with Svelte 3

Andy Bell's progressive disclosure component - the vanilla JS web component implemented as a Svelte web component.

npx degit sveltejs/template disclosure
cd disclosure

Make the indicated modifications to rollup.config.js:

  • Replace the 'iife' (immediatley invoked function execution) output.format with 'es' (ECMAScript module file). can be removed for an ES module.
  • Replace 'public/build/bundle.js' for output.file with 'public/build/disclosure.js'.
  • Replace the svelte plugin compiler options with just { customElement: true, }.
export default {
  input: 'src/main.js',
  output: {
    sourcemap: true,
    format: 'es',
    file: 'public/build/disclosure.js'
  plugins: [
      customElement: true,


  • Replace public/index.html.
  • Replace public/global.css.
  • Replace src/main.js.
  • Delete src/App.svelte.
  • Place src/DisclosureToggle.svelte.

To serve public/index.html:

npm run dev


  • Open the browser at http://localhost:5000/.
  • Toggle the disclosure component.
  • When using Chrome DevTools go to the "Customize and control DevTools" menu (a vertical ellipsis on Version 80)
    • Click on Run command
    • At the > prompt type "javascript"
    • Click on [Debugger] Disable JavaScript
    • Reload the page

... and observe how no information is "lost" in the absense of a functional component.

To build the production version of the disclosure.js module:

npm run build
<!-- file: src/DisclosureToggle.svelte -->
<svelte:options tag={ null }/>
import { onMount } from 'svelte';
// prop names ("buttonLabel") aren't converted to
// attribute names ("button-label")
export let label = 'Toggle content';
let trigger;
let panelInner;
// Initially collapsed
let triggerExpanded = false;
// Initialized during onMount
let maxHeightStyle = '';
// Looks for content within the panel and calculates its height.
// This makes the transition smoother.
function setMaxHeight() {
const height = panelInner.getBoundingClientRect().height;
maxHeightStyle = `--panel-max-height: ${height}px;`
// Sets the relevant aria role based on its current value.
// Sets it to false by default if no value is set
function setTriggerState() {
const name = 'aria-expanded';
// If it's not set yet, set it to true so it is reversed below
const current = trigger.hasAttribute(name) ?
trigger.getAttribute(name) === 'true' :
triggerExpanded = !current;
trigger.setAttribute(name, triggerExpanded.toString());
function toggle(_event) {
class:trigger-expanded={ triggerExpanded }
on:click={ toggle }
bind:this={ trigger }
{ label }
<svg viewBox="0 0 512 512" aria-hidden="true" fill="currentColor" width="1em" height="1em">
<path d="M60 99.333l196 196 196-196 60 60-256 256-256-256z"></path>
<div class="panel" style={ maxHeightStyle }>
<article class="panel__inner" bind:this={ panelInner }>
hr {
border: none;
border-top: 1px dashed #ccc;
margin: 1.5rem 0;
.link-button {
display: inline-flex;
align-items: center;
text-decoration: underline;
text-decoration-skip-ink: auto;
background: transparent;
padding: 0;
margin: 0;
border: 0;
color: #98b06f;
font-size: 1rem;
cursor: pointer;
-webkit-appearance: none;
-moz-appearance: none;
.link-button svg {
opacity: 0.8;
margin-left: 0.5rem;
transition: all 250ms ease-in-out;
font-size: 0.8rem;
/* Need to use class .trigger-expanded
instead of [aria-expanded="true"]
so Svelte CSS compiler includes this rule
.link-button.trigger-expanded svg {
transform: rotate(-180deg);
.link-button:focus {
text-decoration: none;
.link-button:focus {
text-decoration: none;
outline: 1px solid rgba(255, 255, 255, 0.4);
outline-offset: 0.6rem;
.link-button:active {
transform: scale(0.99);
.panel {
--panel-max-height: 500px;
transition: all 200ms ease;
position: relative;
overflow-y: auto;
overflow-x: hidden;
visibility: hidden;
max-height: 0;
-webkit-overflow-scrolling: touch;
.panel__inner {
transition: all 500ms ease;
transition-delay: 50ms;
opacity: 0;
transform: translateY(1rem);
padding-top: 1.5rem;
.trigger-expanded + .panel {
max-height: var(--panel-max-height);
visibility: visible;
.trigger-expanded + .panel .panel__inner {
opacity: 1;
transform: translateY(0);
/* file: public/global.css */
/* General presentation styles */
html {
height: 100%;
background: #212d40;
.link-button {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
body {
height: 93%;
padding: 1.5rem;
line-height: 1.4;
color: #f3f3f3;
main {
flex: 1 0 auto;
footer {
font-size: 0.6rem;
p {
margin: 0;
article > * + * {
margin-top: 1.5em;
a:not([class]) {
color: currentcolor;
a:not([class]):focus {
text-decoration: none;
.container {
display: flex;
flex-direction: column;
height: 100%;
max-width: 30rem;
margin: 0 auto;
.terms {
--flow-space: 2rem;
/* Flow utility, */
.flow {
--flow-space: 1em;
.flow > * + * {
margin-top: var(--flow-space);
/* Button component */
.button {
display: inline-block;
border: none;
padding: 0.6rem 1.2rem 0.8rem 1.2rem;
text-decoration: none;
background: #98b06f;
color: #212d40;
font-size: 1.2rem;
font-weight: 700;
cursor: pointer;
text-align: center;
transition: background 250ms ease-in-out, transform 150ms ease;
-webkit-appearance: none;
-moz-appearance: none;
.button:focus {
background: #ffffff;
.button:focus {
outline: 1px solid #212d40;
outline-offset: -4px;
.button:active {
transform: scale(0.99);
<!DOCTYPE html>
<html lang="en">
<!-- file: public/index.html -->
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Progressive disclosure, the web component demo</title>
<link rel='icon' type='image/png' href='/favicon.png'>
<link rel='stylesheet' href='/global.css'>
<script type="module">
import { DisclosureToggle } from '/build/disclosure.js';
const name = 'disclosure-toggle';
if ('customElements' in self && !customElements.get(name)) {
console.log(`Defining custom element: <${name}>`);
customElements.define(name, DisclosureToggle);
<div class="container">
<main class="flow">
<h1>Buy this thing!</h1>
<p>It is very good and there's absolutely no hidden exceptions!</p>
<a href="//" class="button">Buy it now!!</a>
<div class="terms">
<disclosure-toggle label="See the terms and conditions" class="flow">
Here's some secret terms and conditions that we
didn't want you to see because they explain how
the product isn't actually very good.
Lorem ipsum dolor sit amet, appareat pertinax et
ius, ne pro nibh consulatu consetetur, nulla virtute
definitiones nec in. Ad consul feugait eligendi mea,
mutat tamquam ei mei. No hinc graecis phaedrum pro,
cu erat ipsum sed, ut novum dissentiunt ullamcorper
pro. <a href="#">Dicat aliquid dissentias</a> in per,
meis alterum quaestio mei eu, vero praesent ex eam.
Ei nam homero noluisse dissentiunt, ut vim quot
putent. Vis elitr accusam accommodare id, cu usu
quaestio conceptam, habeo tibique placerat eos cu.
An nullam corpora consulatu qui, graeci euripidis
est et.
In tantas scripta nominati quo, ne essent maluisset
voluptaria nam. Dicat putent feugiat ei sed, te vis
delicata gubergren honestatis, ius liber blandit
delicata ut. Vix et dictas detracto voluptua,
vis an inani dicunt. Qui inciderint intellegebat ea,
cetero verear cu duo.
Vix id etiam sapientem. Vix case velit feugait eu.
Id diceret delenit perpetua vis, has sale utamur
aeterno te. Cum et consetetur mediocritatem.
<a href="">Andy Bell: A progressive disclosure component</a>
// file: src/main.js
import DisclosureToggle from './DisclosureToggle.svelte';
export {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.