ARTICLE AD BOX
This is a very interesting case.
According to https://www.w3.org/TR/css-grid-1/#track-sizes
If the sum of the flex factors is less than 1, they’ll take up only a corresponding fraction of the leftover space, rather than expanding to fill the entire thing.
So when the transition goes to fraction fr frames such as 0.5fr, the inner content won't take the full height of the container, making it "two different transition".
class Accordion {
constructor(container) {
this.container = container;
this.init();
}
init() {
this.container.querySelectorAll('h2, h3').forEach(h => {
h.setAttribute('tabindex', '0');
});
this.container.addEventListener('click', (e) => {
const heading = e.target.closest('h2, h3');
if (heading) {
this.toggle(heading);
}
});
this.container.addEventListener('keydown', (e) => {
if (e.target.matches('h2, h3') && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault();
this.toggle(e.target);
}
});
}
toggle(heading) {
const item = heading.parentElement;
const isH2 = heading.tagName === 'H2';
const isOpen = item.classList.contains('open');
if (isH2) {
this.container.querySelectorAll('.panel.open').forEach(h2 => {
if (h2 !== item) {
h2.classList.remove('open');
h2.querySelectorAll('.sub-panel.open').forEach(h3 => {
h3.classList.remove('open');
});
}
});
if (isOpen) {
item.classList.remove('open');
item.querySelectorAll('.sub-panel.open').forEach(h3 => {
h3.classList.remove('open');
});
} else {
item.classList.add('open');
}
} else {
item.parentElement.parentElement.querySelectorAll('.sub-panel.open').forEach(h3 => {
if (h3 !== item) h3.classList.remove('open');
});
item.classList.toggle('open');
}
}
}
new Accordion(document.querySelector('.accordion'));
.accordion {
margin: 0 auto 2rem;
max-width: 800px;
}
.accordion .panel {
background: #dfd6c9;
}
.accordion > div:not(fist-of-type) {
margin-bottom: 10px;
}
h2, h3 {
cursor: pointer;
color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
margin: 0;
padding: 1rem 1.5rem;
transition: background-color 0.3s ease;
user-select: none;
}
h2 {
background: #1d7fc7;
font-size: 20px;
font-weight: 600;
}
h3 {
background: #c91a5c;
font-size: 18px;
font-weight: 500;
padding-left: 2.5rem;
}
h2:hover, h3:hover {
background-color: #222;
}
.open > h2 {
background-color: #000;
}
.open > h3 {
background-color: #111;
}
h2 > span,
h3 > span {
display: inline-block;
width: 1.5rem;
height: 1.5rem;
position: relative;
flex-shrink: 0;
}
h2 > span::before,
h3 > span::before,
h2 > span::after,
h3 > span::after {
content: '';
position: absolute;
background: #fff;
transition: transform 0.3s ease;
}
h2 > span::before,
h3 > span::before {
width: 12px;
height: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
h2 > span::after,
h3 > span::after {
width: 2px;
height: 12px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.open > h2 > span::after,
.open > h3 > span::after {
transform: translate(-50%, -50%) rotate(90deg);
}
h2 + div,
h3 + div {
display: grid;
grid-template-rows: 0.5fr;
transition: grid-template-rows 1s;
}
.open > h2 + div,
.open > h3 + div {
grid-template-rows: 1fr;
}
h2 + div > div,
h3 + div > div {
overflow: hidden;
min-height: 0;
}
h2 + div p,
h3 + div p {
margin: 0.5rem 0;
padding: 0 1.5rem;
}
h3 + div p {
padding: 0 2.5rem;
}
/* LAYOUT */
* {
box-sizing: border-box;
}
body {
padding: 0 20px 20px;
font-family: system-ui, -apple-system, sans-serif;
}
<div class="accordion">
<div class="panel">
<h2>
Main section 1
<span></span>
</h2>
<div>
<div>
<div class="sub-panel">
<h3>
Subsection 1.1
<span></span>
</h3>
<div>
<div>
<p>Contents of the subsection 1.1</p>
</div>
</div>
</div>
<div class="sub-panel">
<h3>
Subsection 1.2
<span></span>
</h3>
<div>
<div>
<p>Contents of the subsection 1.2</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
If you use absolute units such as px or vh, the height of the container and content will always be "synchronized".
A limitedly-availabile feature, calc-size, can be used to achieve your expected behavior.
class Accordion {
constructor(container) {
this.container = container;
this.init();
}
init() {
this.container.querySelectorAll('h2, h3').forEach(h => {
h.setAttribute('tabindex', '0');
});
this.container.addEventListener('click', (e) => {
const heading = e.target.closest('h2, h3');
if (heading) {
this.toggle(heading);
}
});
this.container.addEventListener('keydown', (e) => {
if (e.target.matches('h2, h3') && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault();
this.toggle(e.target);
}
});
}
toggle(heading) {
const item = heading.parentElement;
const isH2 = heading.tagName === 'H2';
const isOpen = item.classList.contains('open');
if (isH2) {
this.container.querySelectorAll('.panel.open').forEach(h2 => {
if (h2 !== item) {
h2.classList.remove('open');
h2.querySelectorAll('.sub-panel.open').forEach(h3 => {
h3.classList.remove('open');
});
}
});
if (isOpen) {
item.classList.remove('open');
item.querySelectorAll('.sub-panel.open').forEach(h3 => {
h3.classList.remove('open');
});
} else {
item.classList.add('open');
}
} else {
item.parentElement.parentElement.querySelectorAll('.sub-panel.open').forEach(h3 => {
if (h3 !== item) h3.classList.remove('open');
});
item.classList.toggle('open');
}
}
}
new Accordion(document.querySelector('.accordion'));
.accordion {
margin: 0 auto 2rem;
max-width: 800px;
}
.accordion .panel {
background: #dfd6c9;
}
.accordion > div:not(fist-of-type) {
margin-bottom: 10px;
}
h2, h3 {
cursor: pointer;
color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
margin: 0;
padding: 1rem 1.5rem;
transition: background-color 0.3s ease;
user-select: none;
}
h2 {
background: #1d7fc7;
font-size: 20px;
font-weight: 600;
}
h3 {
background: #c91a5c;
font-size: 18px;
font-weight: 500;
padding-left: 2.5rem;
}
h2:hover, h3:hover {
background-color: #222;
}
.open > h2 {
background-color: #000;
}
.open > h3 {
background-color: #111;
}
h2 > span,
h3 > span {
display: inline-block;
width: 1.5rem;
height: 1.5rem;
position: relative;
flex-shrink: 0;
}
h2 > span::before,
h3 > span::before,
h2 > span::after,
h3 > span::after {
content: '';
position: absolute;
background: #fff;
transition: transform 0.3s ease;
}
h2 > span::before,
h3 > span::before {
width: 12px;
height: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
h2 > span::after,
h3 > span::after {
width: 2px;
height: 12px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.open > h2 > span::after,
.open > h3 > span::after {
transform: translate(-50%, -50%) rotate(90deg);
}
h2 + div,
h3 + div {
display: grid;
height: 0;
overflow: hidden;
transition: height 1s;
}
.open > h2 + div,
.open > h3 + div {
height: calc-size(auto, size);
}
h2 + div > div,
h3 + div > div {
overflow: hidden;
min-height: 0;
}
h2 + div p,
h3 + div p {
margin: 0.5rem 0;
padding: 0 1.5rem;
}
h3 + div p {
padding: 0 2.5rem;
}
/* LAYOUT */
* {
box-sizing: border-box;
}
body {
padding: 0 20px 20px;
font-family: system-ui, -apple-system, sans-serif;
}
<div class="accordion">
<div class="panel">
<h2>
Main section 1
<span></span>
</h2>
<div>
<div>
<div class="sub-panel">
<h3>
Subsection 1.1
<span></span>
</h3>
<div>
<div>
<p>Contents of the subsection 1.1</p>
</div>
</div>
</div>
<div class="sub-panel">
<h3>
Subsection 1.2
<span></span>
</h3>
<div>
<div>
<p>Contents of the subsection 1.2</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
Unfortunately, this only works for Chromium > 129.
