I developed an HTML code and I intend to display the text related to each link with a fade-in animation when the link is clicked.
In this code, I have a card with two columns. The first column contains 5 links, and the second column contains 5 text elements wrapped in
tags. In the second column, only one
tag should be visible at a time.
When I don’t use the display style, all the
tags remain in the DOM, and the fade-in animation works correctly. However, all the text and
tags that are not related to the active link should not be displayed. For this purpose, when I use the display style, the fade-in animation doesn’t work properly.
const links = document.querySelectorAll('.links a');
const content = document.querySelectorAll('.content p');
function showContent(index) {
links.forEach((link, i) => {
if (i === index - 1) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
content.forEach((c, i) => {
if (i === index - 1) {
c.classList.add('active');
} else {
c.classList.remove('active');
}
});
}
.container {
display: flex;
width: 80%;
margin: 40px auto;
}
.links {
width: 20%;
background-color: #f0f0f0;
padding: 20px;
}
.links a {
text-decoration: none;
color: #000;
padding: 10px;
display: block;
border-bottom: 1px solid #ccc;
}
.links a.active {
background-color: #ff4444;
color: #fff;
}
.content {
width: 80%;
padding: 20px;
}
.content p {
font-size: 18px;
color: #333;
opacity: 0;
transition: opacity 0.5s ease-in-out;
display: none;
}
.content p.active {
display: block;
opacity: 1;
}
<div class="container">
<div class="links">
<a href="#" class="active" onclick="showContent(1)">Link (1)</a>
<a href="#" onclick="showContent(2)">Link (2)</a>
<a href="#" onclick="showContent(3)">Link (3)</a>
<a href="#" onclick="showContent(4)">Link (4)</a>
<a href="#" onclick="showContent(5)">Link (5)</a>
</div>
<div class="content">
<p id="content-1" class="active">Text related with Link (1)</p>
<p id="content-2">Text related with Link (2)</p>
<p id="content-3">Text related with Link (3)</p>
<p id="content-4">Text related with Link (4)</p>
<p id="content-5">Text related with Link (5)</p>
</div>
</div>
2
Firefox cannot animate display
display
can be animated with transition-behavior: allow-discrete;
and @starting-style
for all the major browsers sans Firefox.
There are three examples:
- JavaScript with
<button>
s - CSS with
<a>
s - CSS with
<input type="radio">
s &<label>
s
Example 1 uses Event Delegation — the event handler listens for “click” events but only actually reacts whenever a <button>
is clicked. All examples use the following CSS:
/**
* Default state.
*/
position: absolute;
visibility: hidden;
opacity: 0;
/**
* Active state.
*/
position: static;
visibility: visible;
opacity: 0;
transition:
0.2s position,
0.7s visibility ease-in,
0.7s opacity ease-in
Instead of using display: block/none
which ignores animation, use:
position: absolute/static
to bring the elements in and out of the document flowvisibility: hidden/visible
to mask the flashing of content
Details are commented in the examples.
JavaScript
<button></button>
// Reference section.box
const box = document.querySelector(".box");
/**
* This "click" event handler toggles the .active class on
* the clicked <button> and the corresponding <article>
* @param {object} event - Event object
*/
const fadeInItem = (event) => {
// event.target determines what the user actually clicked
const clk = event.target;
/**
* If the user clicked a <button>...
*/
if (clk.matches("button")) {
// make an array of all <button>s...
const btns = [...document.querySelectorAll("button")];
// make an array of all <article>s...
const arts = [...document.querySelectorAll("article")];
// remove .active from all <button>s and <article>s...
btns.forEach((btn, i) => {
btn.classList.remove("active");
arts[i].classList.remove("active");
});
// find the index number of the <button> the user clicked...
const idx = btns.indexOf(clk);
// add .active to the <button> the user clicked and...
btns[idx].classList.add("active");
// to the <article> at the same index number.
arts[idx].classList.add("active");
}
};
/**
* Register .box to listen for the "click" event if fired on
* itself and any of it's children. When triggered, it will
* will call function fadeInItem().
*/
box.addEventListener("click", fadeInItem);
:root {
font: 2.5vmax/1.2 "Segoe UI"
}
.box {
display: flex;
margin: 5rem auto;
}
nav {
background-color: #f0f0f0;
}
menu {
list-style: none;
padding: 0
}
li {
border-bottom: 1px solid #ccc;
}
button {
display: block;
padding: 0.75rem;
border: 0;
}
.content {
padding: 0.75rem;
}
/**
* PROPERTIES/VALUES
* position: absolute is the default state of <article>
* which takes it out of the document flow -- it doesn't
* take up space in the DOM, hence shifting is minimal.
*/
article {
position: absolute;
font-size: 1.25rem;
visibility: hidden;
opacity: 0;
}
button.active {
background-color: #ff4444;
color: #fff;
}
/**
* PROPERTIES/VALUES
* The transition of position to/from static/absolute
* should be quick (ex. 0.2ms) so that it's sudden
* insertion/removal to/from the DOM will not linger and
* give off a flash of the content that's leaving. The
* visibility to/from visible/hidden is there to mask any
* shifting of content.
*/
article.active {
position: static;
visibility: visible;
opacity: 1;
transition:
position 0.2s,
visibility 0.7s ease-in,
opacity 0.7s ease-in;
}
<section class="box">
<nav>
<menu>
<li>
<button class="active">Link (1)</button>
</li>
<li>
<button>Link (2)</button>
</li>
<li>
<button>Link (3)</button>
</li>
<li>
<button>Link (4)</button>
</li>
<li>
<button>Link (5)</button>
</li>
</menu>
</nav>
<section class="content">
<article class="active">
<p>Text related with Link (1)</p>
</article>
<article>
<p>Text related with Link (2)</p>
</article>
<article>
<p>Text related with Link (3)</p>
</article>
<article>
<p>Text related with Link (4)</p>
</article>
<article>
<p>Text related with Link (5)</p>
</article>
</section>
</section>
CSS
<a href="#ID" id="ID"></a>
:root {
font: 2.5vmax/1.2 "Segoe UI"
}
/**
* PROPERTIES/VALUES
* When an <a> is clicked everything jumps to the top.
* position: relative/top: 5rem allows everything to shift
* to the top but everything will stop shifting after that.
* The flex-items (ex. <a>) will flow vertically due to
* flex-flow: column... (aka flex-direction).
*/
.box {
position: relative;
top: 5rem;
display: flex;
flex-flow: column nowrap;
width: 80%;
margin: auto;
}
nav {
background-color: #f0f0f0;
}
menu {
list-style: none;
padding: 0;
}
li {
border-bottom: 1px solid #ccc;
}
a {
display: block;
width: 20%;
padding: 0.75rem;
border-bottom: 2px solid #ccc;
text-decoration: none;
}
/**
* PROPERTIES/VALUES
* position: absolute/left: 30% takes it out of document
* flow so it doesn't sit under the <a>s (because of flex
* column direction of .box) and instead sits to the right
* of the <a>s
*/
.content {
position: absolute;
left: 30%;
margin: -0.75rem 0 0;
}
a:link,
a:visited {
color: #000;
}
a:target {
background-color: #ff4444;
color: #fff;
}
/**
* PROPERTIES/VALUES
* position: absolute is the default state of <article>
* which takes it out of the document flow -- it doesn't
* take up space in the DOM, hence shifting is minimal.
*/
article {
position: absolute;
font-size: 1.125rem;
visibility: hidden;
opacity: 0;
}
/**
* SELECTORS
* If #item{N} <a> is clicked, find the next .content
* then find the <article> that's the :nth-child({N}).
*/
/**
* PROPERTIES/VALUES
* The transition of position to/from static/absolute
* should be quick (ex. 0.2ms) so that it's sudden
* insertion/removal to/from the DOM will not linger and
* give off a flash of the content that's leaving. The
* visibility to/from visible/hidden is there to mask any
* shifting of content.
*/
a#item1:target~.content article:nth-child(1),
a#item2:target~.content article:nth-child(2),
a#item3:target~.content article:nth-child(3),
a#item4:target~.content article:nth-child(4),
a#item5:target~.content article:nth-child(5) {
position: static;
visibility: visible;
opacity: 1;
transition:
position 0.2s,
visibility 0.7s ease-in,
opacity 0.7s ease-in;
}
<section class="box">
<!--
<a href="#ID" id="ID"></a>
Due to the cascading nature of CSS, the <a> must be
placed before and adjacent to either the target
(ex. <article>) or ancestors of said target
(ex. .content). Each <a>'s [href] is targeting it's own
[id] so that when clicked it's :target state will stay
until another `<a>`'s :target state is activated.
An <a>'s :target state will change the <article> that
is in the same position as the <a> that has the current
:target state.
-->
<a href="#item1" id="item1">Link (1)</a>
<a href="#item2" id="item2">Link (2)</a>
<a href="#item3" id="item3">Link (3)</a>
<a href="#item4" id="item4">Link (4)</a>
<a href="#item5" id="item5">Link (5)</a>
<section class="content">
<article>
<p>Text related with Link (1)</p>
</article>
<article>
<p>Text related with Link (2)</p>
</article>
<article>
<p>Text related with Link (3)</p>
</article>
<article>
<p>Text related with Link (4)</p>
</article>
<article>
<p>Text related with Link (5)</p>
</article>
</section>
</section>
CSS
<input id="ID" type="radio">
&
<label for="ID"></label>
:root {
font: 2.5vmax/1.2 "Segoe UI"
}
.box {
display: flex;
width: 80vw;
margin: 5rem auto;
}
[name="items"] {
display: none
}
nav {
background-color: #f0f0f0;
}
menu {
list-style: none;
padding: 0;
}
li {
border-bottom: 1px solid #ccc;
}
label {
display: block;
padding: 0.75rem;
}
.content {
padding: 0.75rem;
}
/**
* PROPERTIES/VALUES
* position: absolute is the default state of <article>
* which takes it out of the document flow -- it doesn't
* take up space in the DOM, hence shifting is minimal.
*/
article {
position: absolute;
font-size: 1.125rem;
visibility: hidden;
opacity: 0;
}
/**
* SELECTORS
* If #item{N} radio button is :checked, find the first
* ~ <nav> after it, then find the <menu> inside of the
* <nav>, then find the <li> inside of the <menu> that is
* the :nth-child({N}).
*/
#item1:checked~nav menu li:nth-child(1),
#item2:checked~nav menu li:nth-child(2),
#item3:checked~nav menu li:nth-child(3),
#item4:checked~nav menu li:nth-child(4),
#item5:checked~nav menu li:nth-child(5) {
background-color: #ff4444;
color: #fff;
}
/**
* SELECTORS
* If #item{N} is radio button is :checked, find the
* first ~ .content after it then find the <article>
* that's the :nth-child({N}).
*/
/**
* The transition of position to/from static/absolute
* should be quick (ex. 0.2ms) so that it's sudden
* insertion/removal to/from the DOM will not linger and
* give off a flash of the content that's leaving. The
* visibility to/from visible/hidden is there to mask any
* shifting of content.
*/
#item1:checked~.content article:nth-child(1),
#item2:checked~.content article:nth-child(2),
#item3:checked~.content article:nth-child(3),
#item4:checked~.content article:nth-child(4),
#item5:checked~.content article:nth-child(5) {
position: static;
visibility: visible;
opacity: 1;
transition:
position 0.2s,
visibility 0.7s ease-in,
opacity 0.7s ease-in;
}
<section class="box">
<!--
<input id="ID" type="radio">
All radio buttons are invisible and not within
the DOM. Although technically they're not there,
they still influence whatever is after them in the
DOM by whether they are :checked or not.
-->
<input id="item1" name="items" type="radio" checked>
<input id="item2" name="items" type="radio">
<input id="item3" name="items" type="radio">
<input id="item4" name="items" type="radio">
<input id="item5" name="items" type="radio">
<nav>
<menu>
<!--
<label for="ID"></label>
Each <label> is associated to the radio button
by the [for] attribute's value which is the #id
of said radio button. This association allows the
radio button to be un/checked by the user clicking
the associated <label>.
-->
<li>
<label for="item1">Link (1)</label>
</li>
<li>
<label for="item2">Link (2)</label>
</li>
<li>
<label for="item3">Link (3)</label>
</li>
<li>
<label for="item4">Link (4)</label>
</li>
<li>
<label for="item5">Link (5)</label>
</li>
</menu>
</nav>
<section class="content">
<article>
<p>Text related with Link (1)</p>
</article>
<article>
<p>Text related with Link (2)</p>
</article>
<article>
<p>Text related with Link (3)</p>
</article>
<article>
<p>Text related with Link (4)</p>
</article>
<article>
<p>Text related with Link (5)</p>
</article>
</section>
</section>
CSS has not supported animation of the display
property until quite recently. Most major browsers have added support for transition-behavior: allow-discrete
and @starting-style
, which are the tools you need to solve this problem. (Firefox is the exception here, but I would proceed on the basis that it will get there soonish.)
document.querySelector('button').addEventListener('click', evt => {
evt.target.closest('section').classList.toggle('active')
})
:root {
--box-width: 60px;
}
body, button {
font-family: sans-serif;
font-size: 16px;
}
button {
color: white;
background: blue;
border: 0;
border-radius: 0.5em;
padding: 0.5em 1em;
cursor: pointer;
}
div {
display: flex;
gap: 0.5em;
}
span {
color: white;
background: red;
width: var(--box-width);
aspect-ratio: 1;
display: flex;
justify-content: center;
align-items: center;
transition: 0.7s;
}
.x {
display: none;
width: 0;
opacity: 0;
transition-behavior: allow-discrete;
}
.active .x {
display: flex;
opacity: 1;
width: var(--box-width);
@starting-style {
opacity: 0;
width: 0;
}
}
<section>
<p><button>Toggle</button></p>
<div>
<span>1</span>
<span class="x">2</span>
<span>3</span>
</div>
</section>
5
Using opacity should be fine in this case. I changed the function a bit, so that it is only the anchor that gets a new class name. The content is shown using a CSS selector where I combine :has() with the next-sibling combinator.
const links = document.querySelector('.links');
links.addEventListener('click', e => {
e.preventDefault();
if (e.target.nodeName == 'A') {
let id = e.target.getAttribute('href');
showContent(id);
}
});
function showContent(id) {
links.querySelectorAll('a').forEach(link => link.classList.remove('active'));
links.querySelector(`a[href="${id}"]`).classList.add('active');
}
.container {
display: flex;
width: 80%;
margin: 40px auto;
}
.links {
width: 20%;
background-color: #f0f0f0;
padding: 20px;
}
.links a {
text-decoration: none;
color: #000;
padding: 10px;
display: block;
border-bottom: 1px solid #ccc;
}
.links a.active {
background-color: #ff4444;
color: #fff;
}
.content {
width: 80%;
padding: 20px;
position: relative;
}
.content p {
font-size: 18px;
color: #333;
transition: opacity 0.5s ease-in-out;
position: absolute;
top: 0;
}
div.links:has(a)+div.content>p {
opacity: 0;
}
div.links:has(a.active[href="1"])+div.content>#content-1,
div.links:has(a.active[href="2"])+div.content>#content-2,
div.links:has(a.active[href="3"])+div.content>#content-3,
div.links:has(a.active[href="4"])+div.content>#content-4,
div.links:has(a.active[href="5"])+div.content>#content-5 {
opacity: 1;
}
<div class="container">
<div class="links">
<a href="1" class="active">Link (1)</a>
<a href="2">Link (2)</a>
<a href="3">Link (3)</a>
<a href="4">Link (4)</a>
<a href="5">Link (5)</a>
</div>
<div class="content">
<p id="content-1" class="active">Text related with Link (1)</p>
<p id="content-2">Text related with Link (2)</p>
<p id="content-3">Text related with Link (3)</p>
<p id="content-4">Text related with Link (4)</p>
<p id="content-5">Text related with Link (5)</p>
</div>
</div>