Skip to content

Like Button

A like button with a springy heart and a count that follows the pressed state. The count is an sv-text-flow, so the changed digit flows into place instead of snapping. It is derived on every pressedchange, the base number plus one while pressed, so it can never drift out of sync with the state.

The heart is aria-hidden decoration and a visually hidden "likes" follows the count, so the button announces itself as "1,024 likes" plus the pressed state.

Reduced Motion

Your system is set to reduce motion, so this animation will not play for you.

Code

The following example assumes that you have imported the component and set up the theme.

Like Button
<div class="like-row">
20 collapsed lines
<sv-toggle class="like-toggle">
<span class="heart-stack" aria-hidden="true">
<!-- Outline heart -->
<svg class="icon outline" viewBox="0 -960 960 960" xmlns="http://www.w3.org/2000/svg">
<path
d="m480-120-58-52q-101-91-167-157T150-447.5Q111-500 95.5-544T80-634q0-94 63-157t157-63q52 0 99 22t81 62q34-40 81-62t99-22q94 0 157 63t63 157q0 46-15.5 90T810-447.5Q771-395 705-329T538-172l-58 52Zm0-108q96-86 158-147.5t98-107q36-45.5 50-81t14-70.5q0-60-40-100t-100-40q-47 0-87 26.5T518-680h-76q-15-41-55-67.5T300-774q-60 0-100 40t-40 100q0 35 14 70.5t50 81q36 45.5 98 107T480-228Z"
></path>
</svg>
<!-- Filled heart -->
<svg class="icon filled" viewBox="0 -960 960 960" xmlns="http://www.w3.org/2000/svg">
<path
d="m480-120-58-52q-101-91-167-157T150-447.5Q111-500 95.5-544T80-634q0-94 63-157t157-63q52 0 99 22t81 62q34-40 81-62t99-22q94 0 157 63t63 157q0 46-15.5 90T810-447.5Q771-395 705-329T538-172l-58 52Z"
></path>
</svg>
</span>
<sv-text-flow class="count">1,024</sv-text-flow>
<span class="assistive-text">likes</span>
</sv-toggle>
</div>
<script type="application/x-typescript" module>
8 collapsed lines
const LIKES = 1024;
const toggle = document.querySelector<Toggle>(".like-toggle")!;
const count = toggle.querySelector<TextFlow>(".count")!;
toggle.addEventListener("pressedchange", () => {
count.value = (LIKES + Number(toggle.pressed)).toLocaleString("en-US");
});
</script>
<style>
71 collapsed lines
.like-row {
display: flex;
justify-content: center;
padding-block: 1em;
}
.like-toggle {
--anim-dur: 400ms;
--anim-ease: cubic-bezier(0.486, 1.45, 0.2, 1);
--like-color: #f43f5e;
gap: 0.45em;
padding: 0.4em 0.9em;
color: var(--sv-gray-2);
background-color: transparent;
border-color: transparent;
border-radius: calc(infinity * 1px);
@media (prefers-reduced-motion: reduce) {
--anim-dur: 0s;
}
.heart-stack {
display: grid;
place-items: center;
}
.icon {
display: block;
grid-area: 1 / 1;
inline-size: 1.2em;
block-size: 1.2em;
fill: currentcolor;
transition: scale var(--anim-dur) var(--anim-ease);
}
.icon.filled {
color: var(--like-color);
scale: 0;
}
.count {
font-variant-numeric: tabular-nums;
}
.assistive-text {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
white-space: nowrap;
clip-path: inset(50%);
}
&:hover {
background-color: color-mix(in srgb, var(--like-color) 10%, transparent);
}
&:state(--pressed) {
color: var(--like-color);
background-color: transparent;
.icon.outline {
scale: 0;
}
.icon.filled {
scale: 1;
}
}
}
</style>