I'm working on a Svelte component where I want to create a shiny call-to-action button with gradient animations. The button should have a combination of linear and conic gradients that animate over time. However, the gradient animations are not working as expected.
Here is the code for my Svelte component: Here is the codepen i used https://codepen.io/Daniel-Johan-S-rby/pen/gOVrBbN
<script>
// Her kan du legge til eventuelle Svelte-script hvis nødvendig
</script>
<style>
@import url("https://use.typekit.net/tzv3rms.css");
:root {
--shiny-cta-bg: #000000;
--shiny-cta-bg-subtle: #1a1818;
--shiny-cta-fg: #ffffff;
--shiny-cta-highlight: #3772ff;
--shiny-cta-highlight-subtle: #1a243e;
}
@property --gradient-angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
@property --gradient-angle-offset {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
@property --gradient-percent {
syntax: "<percentage>";
initial-value: 5%;
inherits: false;
}
@property --gradient-shine {
syntax: "<color>";
initial-value: white;
inherits: false;
}
.shiny-cta {
--animation: gradient-angle linear infinite;
--duration: 3s;
--shadow-size: 2px;
isolation: isolate;
position: relative;
overflow: hidden;
cursor: pointer;
outline-offset: 4px;
padding: 1.25rem 2.5rem;
font-family: inherit;
font-size: 1.125rem;
line-height: 1.2;
border: 1px solid transparent;
border-radius: 360px;
color: var(--shiny-cta-fg);
background: linear-gradient(var(--shiny-cta-bg), var(--shiny-cta-bg))
padding-box,
conic-gradient(
from calc(var(--gradient-angle) - var(--gradient-angle-offset)),
transparent,
var(--shiny-cta-highlight) var(--gradient-percent),
var(--gradient-shine) calc(var(--gradient-percent) * 2),
var(--shiny-cta-highlight) calc(var(--gradient-percent) * 3),
transparent calc(var(--gradient-percent) * 4)
)
border-box;
box-shadow: inset 0 0 0 1px var(--shiny-cta-bg-subtle);
}
.shiny-cta::before,
.shiny-cta::after,
.shiny-cta span::before {
content: "";
pointer-events: none;
position: absolute;
inset-inline-start: 50%;
inset-block-start: 50%;
translate: -50% -50%;
z-index: -1;
}
.shiny-cta:active {
translate: 0 1px;
}
.shiny-cta::before {
--size: calc(100% - var(--shadow-size) * 3);
--position: 2px;
--space: calc(var(--position) * 2);
width: var(--size);
height: var(--size);
background: radial-gradient(
circle at var(--position) var(--position),
white calc(var(--position) / 4),
transparent 0
)
padding-box;
background-size: var(--space) var(--space);
background-repeat: space;
mask-image: conic-gradient(
from calc(var(--gradient-angle) + 45deg),
black,
transparent 10% 90%,
black
);
border-radius: inherit;
opacity: 0.4;
z-index: -1;
}
.shiny-cta::after {
--animation: shimmer linear infinite;
width: 100%;
aspect-ratio: 1;
background: linear-gradient(
-50deg,
transparent,
var(--shiny-cta-highlight),
transparent
);
mask-image: radial-gradient(circle at bottom, transparent 40%, black);
opacity: 0.6;
}
.shiny-cta span {
z-index: 1;
}
.shiny-cta span::before {
--size: calc(100% + 1rem);
width: var(--size);
height: var(--size);
box-shadow: inset 0 -1ex 2rem 4px var(--shiny-cta-highlight);
opacity: 0;
}
.shiny-cta {
--transition: 800ms cubic-bezier(0.25, 1, 0.5, 1);
transition: var(--transition);
transition-property: --gradient-angle-offset, --gradient-percent,
--gradient-shine;
}
.shiny-cta,
.shiny-cta::before,
.shiny-cta::after {
animation: var(--animation) var(--duration),
var(--animation) calc(var(--duration) / 0.4) reverse paused;
animation-composition: add;
}
.shiny-cta span::before {
transition: opacity var(--transition);
animation: calc(var(--duration) * 1.5) breathe linear infinite;
}
.shiny-cta:is(:hover, :focus-visible) {
--gradient-percent: 20%;
--gradient-angle-offset: 95deg;
--gradient-shine: var(--shiny-cta-highlight-subtle);
}
.shiny-cta:is(:hover, :focus-visible),
.shiny-cta:is(:hover, :focus-visible)::before,
.shiny-cta:is(:hover, :focus-visible)::after {
animation-play-state: running;
}
.shiny-cta:is(:hover, :focus-visible) span::before {
opacity: 1;
}
@keyframes gradient-angle {
to {
--gradient-angle: 360deg;
}
}
@keyframes shimmer {
to {
rotate: 360deg;
}
}
@keyframes breathe {
from,
to {
scale: 1;
}
50% {
scale: 1.2;
}
}
</style>
<button class="shiny-cta">
<span>Prøv Oss!</span>
</button>
The gradient animations are not working as expected. The button appears, but the gradients do not animate. I have verified that my browser supports CSS Custom Properties and @property.
Verified that CSS Custom Properties are applied correctly by using them in simple styles. Tested basic animations separately to ensure they work. Inspected the element in the browser's developer tools to see if the styles and animations are applied.
There appears to be a scoping issue. By default all selectors and keyframes are scoped to the current component by adding hashes to them but here the animations are referenced in CSS custom properties and thus not adjusted (Svelte does not know that an animation is being referenced).
If the --animation
variable is inlined, it should apply as expected, e.g.
.shiny-cta {
animation:
gradient-angle linear infinite var(--duration),
gradient-angle linear infinite calc(var(--duration) / 0.4) reverse paused;
}
.shiny-cta::after {
animation:
shimmer linear infinite var(--duration),
shimmer linear infinite calc(var(--duration) / 0.4) reverse paused;
}
It is also possible to unscope the @keyframes
, note that this will "pollute" the global namespace of defined animations, so if other components also define unscoped @keyframes
with the same name, there will be conflicts.
To unscope add -global-
before the name:
@keyframes -global-gradient-angle {
to {
--gradient-angle: 360deg;
}
}
@keyframes -global-shimmer {
to {
rotate: 360deg;
}
}
@keyframes -global-breathe {
from,
to {
scale: 1;
}
50% {
scale: 1.2;
}
}
(The usage site should still use the name without this prefix.)