I'm new to CSS and have been playing around with keyframe animations. I currently am using both CSS and JS in order to achieve the following:
apply a keyframe animation when a link is hovered over
apply a second animation when a link is no longer hovered over, but hasn't been clicked
apply a third animation when a link is clicked (and js to delay the nav for a moment so the animation can play, but only the first time it's clicked - after that it returns to normal link behavior)
My problem is that all my links are the a:visited color, even before they're clicked. I'm sure this has to do with how I set up the keyframe animations, but I can't figure out why - the hover-in and hover-out animations begin with the unvisited link color (hover in) and end with currentColor
(hover out). If I change currentColor
to the unvisited link color, the links ignore the a:visited color.
I don't think the on-click animation is at fault, since it only applies after a:active has been used (I also tried a:focus but it didn't seem to make a difference), but it does end on the visited link color.
(There are other issues, like the hover out animation playing on page load despite being paused, and the browser not remembering the visited state between page loads, but I can live with those if the link color behaves.)
Alternately, could it be the js doing this? I don't understand js very well at all, I had a lot of help with it.
ETA: I added animation-fill-mode: none; to the keyframe animations, but it didn't make a difference.
ETA2: fixed link color in css, but keyframe endstate for hover-out still defaults to black
ETA3: Fixed the default color, but there doesn't seem to be a pure-CSS solution to fade the hover-out from black to the current link color - it always defaults to either black, or the reddish color, rather than whatever condition the current link has, a:visited (black) or a:link (reddish).
document.querySelectorAll('a:not(.sitenav a):not(.footer a)').forEach(function (link) {
let isAnimating = false; // Flag to track if animation is already happening
const linkId = link.href; // Use the link's href as a unique identifier
let hasAnimatedOnce = localStorage.getItem(linkId) === 'true'; // Retrieve the value from localStorage
link.addEventListener('click', function (e) {
// If the link has been clicked before (stored in localStorage), allow normal behavior
if (hasAnimatedOnce) {
return; // Normal navigation will happen
}
// If animation is already happening, ignore the click
if (isAnimating) {
return; // Do nothing if animation is in progress
}
// Prevent default navigation behavior to wait for the animation
e.preventDefault();
// Mark that the animation is starting
isAnimating = true;
// Reset the animation by removing it first (so it can be reapplied)
this.style.animation = ''; // Remove the animation to allow it to play again
// Trigger the animation by adding the animation property again
this.style.animation = 'active-animation 3s forwards'; // Adjust timing as necessary
// Store the linkElement to use later in the animationend handler
this._linkElement = this;
});
// Separate animationend listener outside of click event listener
link.addEventListener('animationend', function () {
// If the link element was stored and animation is complete
if (this._linkElement) {
// Now navigate to the link's href after the animation finishes
window.location.href = this._linkElement.href;
// Reset the animation flag so the link can be clicked again
isAnimating = false;
// Mark that the link has been clicked and animated once
hasAnimatedOnce = true;
// Save the state to localStorage so the flag persists across sessions
localStorage.setItem(linkId, 'true');
// Clear the stored link element reference
delete this._linkElement;
}
});
});
/* Base state for the link - initial color is reddish */
a:link {
position: relative; /* Ensure the link is positioned */
z-index: 1; /* Ensure the link is above other elements */
color: #660e0e; /* Start as reddish */
pointer-events: auto;
text-decoration: none;
transition: color 0.4s ease;
}
a:not(.sitenav a):not(.footer a) {
animation-play-state: paused; /* Prevent any animation from starting initially */
pointer-events: auto;
position: relative; /* Ensure the link is positioned */
text-decoration: none;
}
/* Ensuring the link remains clickable during the animation */
a:hover {
pointer-events: auto; /* Ensures pointer events remain enabled */
}
/* Hover-in animation for normal hover effect (only when not clicked) */
a:not(.sitenav a):not(.footer a):not(:active):hover {
animation: hover-in 3s forwards; /* Add hover animation */
}
/* Hover-out animation when the mouse leaves, but only if the link hasn't been clicked */
a:not(.sitenav a):not(.footer a):not(:active):not(:hover) {
animation: hover-out 4s none cubic-bezier(.45,.05,.55,.95);
}
/* visited link */
a:visited {
color: black;
}
/* selected link */
a:not(.sitenav a):not(.footer a):active {
animation-name: active-animation;
animation-duration: 3s;
animation-fill-mode: none;
}
/* Active link animation (bright red -> black) */
@keyframes active-animation {
0% {
color: #b80909; /* Start as bright red */
text-shadow: 0 0 20px darkred, text-shadow: 0 0 5px brown;
}
10% {
font-size: 1.02em; /* embiggen */
color: #b80909;
text-shadow: 0 0 20px darkred, text-shadow: 0 0 10px brown;
}
20% {
color: black; /* Fade back to black */
}
30% {
font-size: 1.04em;
}
60% {
text-shadow: 0 0 10px darkred, 0 0 5px brown;
font-size: ''; /* shrink again */
}
100% {
text-shadow: 0 0 0px darkred, text-shadow: 0 0 0px brown;
color: '';
}
}
/* Hover-in animation (reddish to black with shadow) */
@keyframes hover-in {
0% {
color: ''; /* Start as link or visited */
text-shadow: none;
}
100% {
color: black;
text-shadow: 0 0 30px #850000, 0 0 10px #110402, 0 0 5px #110402;
}
}
/* Hover-out animation (black to prev color with shadow fading) */
@keyframes hover-out {
0% {
color: black;
text-shadow: 0 0 30px #850000, 0 0 10px #110402, 0 0 5px #110402;
}
50% {
color: black;
text-shadow: 0 0 20px #850000, 0 0 5px #110402, 0 0 3px #110402;
}
100% {
color: ''; /* return to prev state? */
}
}
html:
<!doctype html>
<html>
<head>
<script type="text/javascript" src="CC-xxxCSS/CC-scripts.js" defer></script>
<script type="text/javascript" src="CC-xxxCSS/fragment-loader.js" defer></script>
<script type="text/javascript" src="CC-xxxCSS/base-navigation.js" defer></script>
<link rel="stylesheet" type="text/css" href="CC-xxxCSS/CC-styles.css">
<script src=".7.1.slim.js" integrity="sha256-UgvvN8vBkgO0luPSUl2s8TIlOSYRoGFAX4jlCIm9Adc=" crossorigin="anonymous"></script>
</head>
<body>
<div>
<p>
Fonts used on this website:
<p>
<ul>
<li>
<a href=".font">Black Castle MF</a>
<li>
<a href=".font">Blackletter HPLHS</a>
<li>
<a href=".font">Chomsky</a>
<li>
<a href="/">Trajan Pro</a>
<li>
<a href=".font">WW2 Blackletter HPLHS</a>
</ul>
</div>
</body>
</html>
I'm new to CSS and have been playing around with keyframe animations. I currently am using both CSS and JS in order to achieve the following:
apply a keyframe animation when a link is hovered over
apply a second animation when a link is no longer hovered over, but hasn't been clicked
apply a third animation when a link is clicked (and js to delay the nav for a moment so the animation can play, but only the first time it's clicked - after that it returns to normal link behavior)
My problem is that all my links are the a:visited color, even before they're clicked. I'm sure this has to do with how I set up the keyframe animations, but I can't figure out why - the hover-in and hover-out animations begin with the unvisited link color (hover in) and end with currentColor
(hover out). If I change currentColor
to the unvisited link color, the links ignore the a:visited color.
I don't think the on-click animation is at fault, since it only applies after a:active has been used (I also tried a:focus but it didn't seem to make a difference), but it does end on the visited link color.
(There are other issues, like the hover out animation playing on page load despite being paused, and the browser not remembering the visited state between page loads, but I can live with those if the link color behaves.)
Alternately, could it be the js doing this? I don't understand js very well at all, I had a lot of help with it.
ETA: I added animation-fill-mode: none; to the keyframe animations, but it didn't make a difference.
ETA2: fixed link color in css, but keyframe endstate for hover-out still defaults to black
ETA3: Fixed the default color, but there doesn't seem to be a pure-CSS solution to fade the hover-out from black to the current link color - it always defaults to either black, or the reddish color, rather than whatever condition the current link has, a:visited (black) or a:link (reddish).
document.querySelectorAll('a:not(.sitenav a):not(.footer a)').forEach(function (link) {
let isAnimating = false; // Flag to track if animation is already happening
const linkId = link.href; // Use the link's href as a unique identifier
let hasAnimatedOnce = localStorage.getItem(linkId) === 'true'; // Retrieve the value from localStorage
link.addEventListener('click', function (e) {
// If the link has been clicked before (stored in localStorage), allow normal behavior
if (hasAnimatedOnce) {
return; // Normal navigation will happen
}
// If animation is already happening, ignore the click
if (isAnimating) {
return; // Do nothing if animation is in progress
}
// Prevent default navigation behavior to wait for the animation
e.preventDefault();
// Mark that the animation is starting
isAnimating = true;
// Reset the animation by removing it first (so it can be reapplied)
this.style.animation = ''; // Remove the animation to allow it to play again
// Trigger the animation by adding the animation property again
this.style.animation = 'active-animation 3s forwards'; // Adjust timing as necessary
// Store the linkElement to use later in the animationend handler
this._linkElement = this;
});
// Separate animationend listener outside of click event listener
link.addEventListener('animationend', function () {
// If the link element was stored and animation is complete
if (this._linkElement) {
// Now navigate to the link's href after the animation finishes
window.location.href = this._linkElement.href;
// Reset the animation flag so the link can be clicked again
isAnimating = false;
// Mark that the link has been clicked and animated once
hasAnimatedOnce = true;
// Save the state to localStorage so the flag persists across sessions
localStorage.setItem(linkId, 'true');
// Clear the stored link element reference
delete this._linkElement;
}
});
});
/* Base state for the link - initial color is reddish */
a:link {
position: relative; /* Ensure the link is positioned */
z-index: 1; /* Ensure the link is above other elements */
color: #660e0e; /* Start as reddish */
pointer-events: auto;
text-decoration: none;
transition: color 0.4s ease;
}
a:not(.sitenav a):not(.footer a) {
animation-play-state: paused; /* Prevent any animation from starting initially */
pointer-events: auto;
position: relative; /* Ensure the link is positioned */
text-decoration: none;
}
/* Ensuring the link remains clickable during the animation */
a:hover {
pointer-events: auto; /* Ensures pointer events remain enabled */
}
/* Hover-in animation for normal hover effect (only when not clicked) */
a:not(.sitenav a):not(.footer a):not(:active):hover {
animation: hover-in 3s forwards; /* Add hover animation */
}
/* Hover-out animation when the mouse leaves, but only if the link hasn't been clicked */
a:not(.sitenav a):not(.footer a):not(:active):not(:hover) {
animation: hover-out 4s none cubic-bezier(.45,.05,.55,.95);
}
/* visited link */
a:visited {
color: black;
}
/* selected link */
a:not(.sitenav a):not(.footer a):active {
animation-name: active-animation;
animation-duration: 3s;
animation-fill-mode: none;
}
/* Active link animation (bright red -> black) */
@keyframes active-animation {
0% {
color: #b80909; /* Start as bright red */
text-shadow: 0 0 20px darkred, text-shadow: 0 0 5px brown;
}
10% {
font-size: 1.02em; /* embiggen */
color: #b80909;
text-shadow: 0 0 20px darkred, text-shadow: 0 0 10px brown;
}
20% {
color: black; /* Fade back to black */
}
30% {
font-size: 1.04em;
}
60% {
text-shadow: 0 0 10px darkred, 0 0 5px brown;
font-size: ''; /* shrink again */
}
100% {
text-shadow: 0 0 0px darkred, text-shadow: 0 0 0px brown;
color: '';
}
}
/* Hover-in animation (reddish to black with shadow) */
@keyframes hover-in {
0% {
color: ''; /* Start as link or visited */
text-shadow: none;
}
100% {
color: black;
text-shadow: 0 0 30px #850000, 0 0 10px #110402, 0 0 5px #110402;
}
}
/* Hover-out animation (black to prev color with shadow fading) */
@keyframes hover-out {
0% {
color: black;
text-shadow: 0 0 30px #850000, 0 0 10px #110402, 0 0 5px #110402;
}
50% {
color: black;
text-shadow: 0 0 20px #850000, 0 0 5px #110402, 0 0 3px #110402;
}
100% {
color: ''; /* return to prev state? */
}
}
html:
<!doctype html>
<html>
<head>
<script type="text/javascript" src="CC-xxxCSS/CC-scripts.js" defer></script>
<script type="text/javascript" src="CC-xxxCSS/fragment-loader.js" defer></script>
<script type="text/javascript" src="CC-xxxCSS/base-navigation.js" defer></script>
<link rel="stylesheet" type="text/css" href="CC-xxxCSS/CC-styles.css">
<script src="https://code.jquery/jquery-3.7.1.slim.js" integrity="sha256-UgvvN8vBkgO0luPSUl2s8TIlOSYRoGFAX4jlCIm9Adc=" crossorigin="anonymous"></script>
</head>
<body>
<div>
<p>
Fonts used on this website:
<p>
<ul>
<li>
<a href="https://www.1001freefonts/black-castle-mf.font">Black Castle MF</a>
<li>
<a href="http://www.dafont/blackletter-hplhs.font">Blackletter HPLHS</a>
<li>
<a href="https://www.dafont/chomsky.font">Chomsky</a>
<li>
<a href="https://freefontsfamily./trajan-pro-font-free/">Trajan Pro</a>
<li>
<a href="http://www.dafont/ww2blackletter.font">WW2 Blackletter HPLHS</a>
</ul>
</div>
</body>
</html>
Share
Improve this question
edited Feb 2 at 18:01
K-RAM
asked Feb 2 at 12:57
K-RAMK-RAM
137 bronze badges
10
- I've created a Runnable Snippet (currently non-functional since you missed to provide some HTML). Please edit your question in order to provide a minimal reproducible example. Read: How to Ask. – Roko C. Buljan Commented Feb 2 at 13:09
- 2 As a first suggestion: never assign additional eventListeners inside an eventListener - this will incrementally create new listeners on every click. – Roko C. Buljan Commented Feb 2 at 13:11
- Sorry! I've added the html now, I can upload to an actual server if needed, but not sure how to use a jfiddle? – K-RAM Commented Feb 2 at 13:22
- 1 please, add the HTML right into the Runnable snippet, not as a code-block. – Roko C. Buljan Commented Feb 2 at 13:23
- 1 You've got one "isAnimating" flag in your loop and it's shared among all the clickable links. That seems weird. – Pointy Commented Feb 2 at 13:24
1 Answer
Reset to default 0The issue likely occurs because browser styles for :visited links have restrictions for security and privacy reasons. Here's why your a:link color might not be showing properly before a keyframe animation changes it to a:visited:
1️⃣ Browsers Restrict :visited Styling Browsers limit what can be styled on :visited links to prevent websites from detecting which links a user has visited. ✅ Allowed Properties: color, background-color, outline-color, border-color ❌ Not Allowed: opacity, transform, visibility, content, etc.
2️⃣ Keyframe Animation Might Not Work on :visited If your animation involves properties that aren't allowed on :visited, the browser may ignore or override the styles. Solution: Force Proper Styling Before & After Click
document.querySelectorAll("a").forEach(link => {
link.addEventListener("click", function() {
this.style.color = "purple"; // Simulate `:visited` manually
});
});
a:link {
color: blue; /* Default color before click */
}
a:visited {
color: purple; /* Changes after click */
}
a:hover {
color: red; /* Hover effect */
}
a:active {
color: green; /* Click effect */
}
/* Keyframe animation example */
@keyframes fadeColor {
0% { color: blue; }
100% { color: purple; }
}
a:link, a:visited {
animation: fadeColor 2s ease-in-out;
}
Browsers limit :visited styles, so animations might not work as expected. Ensure only color and background-color are animated. If necessary, use JavaScript to control styling dynamically.