Most setups don’t need any JS beyond loading the script. The component auto-initializes every .read-more on the page after DOMContentLoaded. Reach for the JavaScript API when you want to toggle a component from your own code, react to expand and collapse, initialize content added after page load, change the default selectors, or tear an instance down cleanly.
Getting an instance
After auto-init, each root element has its instance attached as _readMore:
const rm = document.querySelector(".read-more")._readMore;
rm.expand();
If you’d rather capture instances at init time, ReadMore.init() returns the ones it creates:
const instances = ReadMore.init(".read-more");
const first = instances[0];
if (first) first.toggle();
Or construct one directly against an element you already have:
const root = document.querySelector(".my-collapse");
const instance = new ReadMore(root, { defaultMaxHeight: 200 });
Instance methods
| Method | Description | Notes |
|---|
expand() | Expand the content. | No-op if already expanded or if the content is short enough that no toggle is needed. Dispatches readMore:will-expand / readMore:will-change immediately, then readMore:expand / readMore:change after the animation completes. |
collapse() | Collapse the content. | No-op if already collapsed or if the content is short enough that no toggle is needed. Dispatches readMore:will-collapse / readMore:will-change immediately, then readMore:collapse / readMore:change after the animation completes. Accepts { fromUser: true } to scroll the component back into view if its top is off-screen. |
toggle() | Flip between expanded and collapsed. | No-op if no toggle is needed. Accepts { fromUser: true }, which is what the trigger click passes, to turn on the keep-your-place scroll. Dispatches the matching pair of events for whichever direction it goes. |
updateCollapseState() | Re-check whether the content needs collapsing and show or hide the trigger accordingly. | Called automatically by the internal ResizeObserver. Call manually after swapping content if the height isn’t being picked up. |
setMaxHeight(value) | Set the inline --content-max-height CSS variable on the content. | Used internally by the animation. Pass null to clear it. Rarely needed in user code. |
destroy() | Remove all listeners and observers, cancel pending timers and animation frames, and restore the original markup. That means the label, inline styles, state classes and any generated ID. Clears the _readMore reference from the root. | Call before removing the root element from the DOM for a clean teardown. |
Instance properties
| Property | Type | Description |
|---|
root | Element | The outer .read-more element. |
content | Element | The content element matched by contentSelector. |
button | Element | The trigger button matched by triggerSelector. |
isExpanded | boolean | Getter that returns true when the root has the expanded class. |
userExpanded | boolean | Whether the user has manually expanded this instance. Persists across reflows. |
maxHeight | number | Getter that reads data-max-height from the root and falls back to defaultMaxHeight. Re-read on every collapse check, so updating the attribute updates the threshold live. |
labels | { more, less } | References to the two label spans inside the trigger. |
labelWidths | { more, less } | The measured pixel widths of each label, used to animate the trigger width. Re-measured when web fonts load, when the type size changes, or when a hidden instance becomes visible. |
Constructor options
Passed as the second argument to new ReadMore(root, options).
| Option | Description | Type | Default | HTML equivalent |
|---|
contentSelector | Selector for the content element inside the root. | string | ".read-more__content" | (none) |
triggerSelector | Selector for the trigger button inside the root. | string | ".read-more-trigger" | (none) |
triggerTextSelector | Selector for the element that holds the label text. Gets replaced with the animated label markup on init. | string | ".read-more-trigger > span" | (none) |
defaultMaxHeight | Fallback collapsed height (in pixels) when the root has no data-max-height attribute. | number | 100 | data-max-height (per-instance override) |
stabilizeMs | How long the content height has to stay stable before the trigger hides on short content. Raise this if your page has late-loading images or fonts that cause height to bounce. | number | 300 | (none) |
Events
All events are dispatched on the root .read-more element and bubble, so you can listen on the root itself or on any ancestor. Every event includes { isExpanded } in its detail.
| Event | When it fires | detail.isExpanded |
|---|
readMore:will-expand | The moment expand intent flips the state, before the animation starts. | true |
readMore:will-collapse | The moment collapse intent flips the state, before the animation starts. | false |
readMore:will-change | Same timing as the matching will-expand / will-collapse. Use this when you don’t care which direction. | true or false |
readMore:expand | After the expand animation completes. | true |
readMore:collapse | After the collapse animation completes. | false |
readMore:change | Same timing as the matching expand / collapse. | true or false |
const rm = document.querySelector(".read-more");
rm.addEventListener("readMore:expand", () => {
// analytics, scrollIntoView, focus management, etc.
});
rm.addEventListener("readMore:change", (e) => {
console.log("now", e.detail.isExpanded ? "expanded" : "collapsed");
});
Static methods
| Method | Description |
|---|
ReadMore.init(rootSelector) | Find every matching root on the page and return an array of instances. Roots that are already initialized return their existing instance instead of being set up twice, so it’s safe to call again after adding content. Defaults to .read-more. This is what runs automatically on DOMContentLoaded. |
Examples
Reach in from another script and react to expand
const rm = document.querySelector(".read-more");
rm.addEventListener("readMore:expand", () => {
// analytics, scrollIntoView, focus management, etc.
});
Drive a component from your own button
const bio = document.querySelector("#bio")._readMore;
document.querySelector("#open-bio").addEventListener("click", () => {
bio.expand();
});
document.querySelector("#close-bio").addEventListener("click", () => {
bio.collapse();
});
Capture instances at init
const instances = ReadMore.init(".read-more");
document.querySelector("#expand-all").addEventListener("click", () => {
instances.forEach((rm) => rm.expand());
});
Initialize content added after page load
// after dropping new .read-more markup into the DOM
const fresh = ReadMore.init(".new-section .read-more");
Custom selectors for non-default markup
const root = document.querySelector(".product-description");
new ReadMore(root, {
contentSelector: ".product-description__body",
triggerSelector: ".product-description__toggle",
triggerTextSelector: ".product-description__toggle .label",
defaultMaxHeight: 240,
});
root.addEventListener("readMore:expand", () => {
console.log("product description expanded");
});
Check current state
const rm = document.querySelector(".read-more")._readMore;
if (rm.isExpanded) {
rm.collapse();
} else {
rm.expand();
}
Re-init after swapping the inner content
// destroy first to clean up listeners and timers
instance.destroy();
// swap in your new content
content.innerHTML = newMarkup;
// build a fresh instance against the same root
const fresh = new ReadMore(root);
Adjust the threshold on resize
// data-max-height is read live, so updating the attribute updates the threshold
window.addEventListener("resize", () => {
const desktop = window.innerWidth >= 768;
document.querySelectorAll(".read-more").forEach((el) => {
el.setAttribute("data-max-height", desktop ? "240" : "120");
});
});
Force a re-check after content changes
// most height changes are picked up automatically by ResizeObserver.
// for the rare case it isn't, nudge it manually.
instance.updateCollapseState();
Clean teardown before removing from the DOM
instance.destroy();
root.remove();