Part III - collapsible content cards in Divi 5 - with JS & CSS
Part 3 of our series on building collapsible content cards in Divi 5. Parts 1 and 2 covered the visual approach using Divi 5 Interactions. This part shows the CSS and JavaScript solution — smoother, cleaner, and with no duplicate content in your HTML.
Why This Approach Is Better Than the Visual Method
In Part 2 we built the expand and collapse behaviour using Divi 5 Interactions with two Text modules — one cropped preview and one full version. That approach works without any code, but it has a fundamental drawback: the same text content exists twice in your HTML. Search engines and screen readers see duplicated content, and every time you duplicate the card you have to manually update the Interaction targets in both buttons.
The CSS and JavaScript approach in this article uses a single Text module. The full text lives in one place. CSS limits how much is visible. JavaScript toggles a class to reveal or hide the rest. No duplicate content, no manual target updates after duplicating.
Additional improvements over the visual method:
- Smooth expand and collapse animation — no snapping
- Soft fade gradient at the bottom of the truncated text
- A single button that dynamically changes its own label
- Works automatically for every card on the page without individual configuration
The Module Structure in Divi 5
The structure is flat and clean. The outer Group module wraps everything and receives the CSS class that the JavaScript uses as its reference point:
Section
└── Row
└── Column → CSS Class: card-outer
└── Group module
├── Heading module
├── Image module
├── Text module → CSS Class: card-text
└── Group module → CSS Class: card-buttons
└── Button → CSS Class: btn-toggle
One Text module. One Button. No Interactions configured anywhere on any element.
How to Assign the CSS Classes
For each element above, open its settings and go to Advanced → Attributes → Add Attribute:
- Attribute Name:
class - Attribute Value: the class name shown in the structure above
The card-outer class goes on the Column, not the Group. To access the Column in Divi 5: click the Row gear icon → properties panel → Content → Elements → Column.
Set an Admin Label in the Content tab of every module. This keeps the Layers panel readable and makes duplication manageable.
The Complete CSS
Paste this into Divi → Theme Options → General → Custom CSS, or into your child theme's style.css:
/* ================================================
STACK CARDS — sticky card layout
================================================ */
/* Settings */
:root {
--navbar: 120px;
--peek: 40px;
}
/* Combine Divi's own class with custom class
to win against Divi's CSS specificity */
.et_pb_row.stack-card {
position: sticky !important;
border-radius: 16px;
margin-bottom: 60vh !important;
z-index: 1;
}
.et_pb_row.stack-card-1 { top: calc(var(--navbar) + 0 * var(--peek)); z-index: 1; }
.et_pb_row.stack-card-2 { top: calc(var(--navbar) + 1 * var(--peek)); z-index: 2; }
.et_pb_row.stack-card-3 { top: calc(var(--navbar) + 2 * var(--peek)); z-index: 3; }
/* Overflow fix for the section container */
.et_pb_section:has(.stack-card) {
overflow: visible !important;
}
/* ================================================
CARD TEXT — collapsible text block
================================================ */
/* Limit the visible height of the text block */
.card-text {
max-height: 96px;
overflow: hidden;
position: relative;
transition: max-height 0.5s ease;
}
/* Soft fade gradient at the bottom edge */
.card-text::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 56px;
background: linear-gradient(transparent, #ffffff);
/* Change #ffffff to match your card background colour */
pointer-events: none;
transition: opacity 0.3s ease;
}
/* Expanded state: release the height limit */
.card-text.is-open {
max-height: 2000px;
}
/* Expanded state: fade the gradient out */
.card-text.is-open::after {
opacity: 0;
}
/* ================================================
CARD BUTTON — toggle button label
================================================ */
/* Stabilise button position and prevent flash on load */
.btn-toggle {
position: relative;
overflow: hidden;
}
/* Freeze the border so it does not animate on load */
.btn-toggle,
.btn-toggle:hover,
.btn-toggle:focus,
.btn-toggle:active {
border-width: 2px !important;
border-style: solid !important;
}
/* Hide the original label set in the Divi builder */
.btn-toggle .et_pb_button_text {
visibility: hidden;
}
/* Show the initial label via CSS using ::before
— leaves Divi's own ::after hover effect untouched */
.btn-toggle::before {
content: 'Read more \2193';
visibility: visible;
position: absolute;
left: 0;
right: 0;
text-align: center;
z-index: 2;
}
/* Switch label when card is in expanded state */
.btn-toggle.is-active::before {
content: 'Read less \2191';
}
The Complete JavaScript
Way 1 — Divi Theme Options (no child theme needed)
Go to Divi → Theme Options → Integration and paste the following into the field "Add code to the \ of your blog":
<script>
document.addEventListener('DOMContentLoaded', function() {
/* Find all toggle buttons on the page */
document.querySelectorAll('.btn-toggle').forEach(function(btn) {
/* Target the inner text element — Divi wraps button text in a span */
var btnText = btn.querySelector('.et_pb_button_text') || btn;
/* Set initial label directly on the text element */
btnText.textContent = 'Read more \u2193';
btn.addEventListener('click', function(e) {
/* Prevent the browser from following the button link */
e.preventDefault();
/* Prevent the click event from bubbling up to parent elements */
e.stopPropagation();
/* Find the card container that holds this button */
var card = this.closest('.card-outer');
/* Find the text block inside that card */
var text = card.querySelector('.card-text');
/* Toggle the expanded state on the text block */
var isOpen = text.classList.toggle('is-open');
/* Update the inner text element directly */
var btnText = this.querySelector('.et_pb_button_text') || this;
btnText.textContent = isOpen ? 'Read less \u2191' : 'Read more \u2193';
});
});
});
</script>
Way 2 — Child Theme (recommended for professional builds)
Create the file wp-content/themes/[your-child-theme]/js/card-toggle.js with this content (no script tags):
document.addEventListener('DOMContentLoaded', function() {
/* Find all toggle buttons on the page */
document.querySelectorAll('.btn-toggle').forEach(function(btn) {
/* Target the inner text element — Divi wraps button text in a span */
var btnText = btn.querySelector('.et_pb_button_text') || btn;
/* Set initial label directly on the text element */
btnText.textContent = 'Read more \u2193';
btn.addEventListener('click', function(e) {
/* Prevent the browser from following the button link */
e.preventDefault();
/* Prevent the click event from bubbling up to parent elements */
e.stopPropagation();
/* Find the card container that holds this button */
var card = this.closest('.card-outer');
/* Find the text block inside that card */
var text = card.querySelector('.card-text');
/* Toggle the expanded state on the text block */
var isOpen = text.classList.toggle('is-open');
/* Update the inner text element directly */
var btnText = this.querySelector('.et_pb_button_text') || this;
btnText.textContent = isOpen ? 'Read less \u2191' : 'Read more \u2193';
});
});
});
Then add this to your child theme's functions.php:
function mytheme_enqueue_card_scripts() {
wp_enqueue_script(
'card-toggle',
get_stylesheet_directory_uri() . '/js/card-toggle.js',
array(),
'1.0.0',
true /* load in footer, before </body> */
);
}
add_action( 'wp_enqueue_scripts', 'mytheme_enqueue_card_scripts' );
Important: if your functions.php already contains a wp_enqueue_scripts function, add the wp_enqueue_script line inside the existing function — do not create a second function with the same name.
Security Notes
Only paste code you have read and understand. JavaScript added to your WordPress site runs on every page for every visitor. Code from unknown sources can steal session data, inject unwanted content, or open your site to attacks. The code in this guide contains no external requests, no data collection, and no eval() calls.
Never use eval(). The eval() function executes arbitrary strings as JavaScript. It is rarely necessary in legitimate code and is a common vector for injected malicious scripts. If you encounter code containing eval(), treat it with caution.
Use wp_enqueue_script, not direct script tags. In functions.php, always register scripts through WordPress's enqueue system. Never output <script> tags directly from PHP — this bypasses WordPress's dependency and caching management.
Increment the version number after updates. When you modify card-toggle.js, change '1.0.0' to '1.0.1' in the enqueue call. Browsers cache JavaScript aggressively — without a version change, visitors may see the old version for hours after your update.
What to Test After Adding the Code
Always test in the live frontend — not in the Divi Visual Builder. The builder loads its own scripts that can interfere with the results. Use a private / incognito window for the most accurate test.
- Initial state — text cropped to approximately four lines, fade gradient visible, button shows "Read more ↓"
- Expand — smooth animation, fade disappears, button switches to "Read less ↑"
- Collapse — smooth animation back, fade reappears, button returns to "Read more ↓"
- Multiple cards — duplicate the card, confirm each button only affects its own card
- Background colour — if your card background is not white, update
#ffffffin the CSS gradient - Mobile — check that 96px still shows approximately four lines at your mobile font size
- No page jump on click — the button should not scroll the page
- No flash on load — button border and label should appear immediately without flickering
One Text Module vs. Two Text Modules — Summary
Visual method (Part 2) CSS + JS method (this article) Code required None CSS + JS Text modules needed Two (duplicate content) One Duplicate content in HTML Yes No After duplicating the card Manual target updates needed Nothing to update Smooth animation No — snaps Yes Fade gradient No Yes Single button with dynamic label No — two buttons needed YesThis post is part of a series on building smarter, more interactive layouts in Divi 5
I’ve put together a small collection of tests on this page. Just a heads-up: it’s all about functionality, not aesthetics! This page isn't going to win any beauty contests, nor is it meant to. Its purpose is simply to show whether certain elements work for the blog posts on diviuniversity.com.
If you’re interested, feel free to rebuild these setups yourself and then use the KrafterPRO presets to create a truly beautiful design. This link will only be active for a short time. Thanks for understanding!
