Hover over the button to see the animation
Osmo CTA Button
inspired by Osmo
Staggerred letter animation on hover, inspired by Osmo's supply button. Implemented in React and Svelte.
Tutorial
ButtonCTAAnimationText AnimationHover Interaction
React
Svelte
CSS
React implementation
OsmoButton.jsx
import styles from "./OsmoButton.module.css";
export default function OsmoButton({text = "Hover over"}){
const letters = text.split("");
return (
<div style={{display: 'flex', gap: 0, cursor: 'pointer'}}>
{letters.map((letter, index) => (
<div key={index} style={{overflow: 'hidden', height: '1em'}}>
<div style={{display: 'inline-block', transitionDelay: `${index * 7}ms`, transition: 'transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1)', transform: 'translateY(0)'}}>
{letter === ' ' ? '\u00A0' : letter}
</div>
<div style={{display: 'inline-block', transitionDelay: `${index * 7}ms`, transition: 'transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1)', transform: 'translateY(0)'}}>
{letter === ' ' ? '\u00A0' : letter}
</div>
</div>
))}
</div>
)
}
import styles from "./OsmoButton.module.css";
export default function OsmoButton({text = "Hover over"}){
const letters = text.split("");
return (
<div style={{display: 'flex', gap: 0, cursor: 'pointer'}}>
{letters.map((letter, index) => (
<div key={index} style={{overflow: 'hidden', height: '1em'}}>
<div style={{display: 'inline-block', transitionDelay: `${index * 7}ms`, transition: 'transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1)', transform: 'translateY(0)'}}>
{letter === ' ' ? '\u00A0' : letter}
</div>
<div style={{display: 'inline-block', transitionDelay: `${index * 7}ms`, transition: 'transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1)', transform: 'translateY(0)'}}>
{letter === ' ' ? '\u00A0' : letter}
</div>
</div>
))}
</div>
)
}
OsmoButton.module.css
.button{
font-family: "Host Grotesk", sans-serif;
font-size: 1.33rem;
font-weight: 500;
background-color: white;
color: black;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
cursor: pointer;
display: inline-flex;
transition: all 0.8s cubic-bezier(0.625, 0.05, 0, 1);
/* box-shadow only for visibility on white backgrounds */
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
--clipHeight: 1.4em;
}
.clipContainer{
position: relative;
display: inline-flex;
height: var(--clipHeight);
overflow: hidden;
flex-direction: column;
}
.text{
position: relative;
display: inline-flex;
align-items: center;
height: var(--clipHeight);
transition: all 0.8s cubic-bezier(0.625, 0.05, 0, 1);
transform: translateY(0);
}
.button:hover{
scale: 0.95;
}
.button:hover .text{
transform: translateY(calc(-1 * var(--clipHeight)));
}
.button{
font-family: "Host Grotesk", sans-serif;
font-size: 1.33rem;
font-weight: 500;
background-color: white;
color: black;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
cursor: pointer;
display: inline-flex;
transition: all 0.8s cubic-bezier(0.625, 0.05, 0, 1);
/* box-shadow only for visibility on white backgrounds */
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
--clipHeight: 1.4em;
}
.clipContainer{
position: relative;
display: inline-flex;
height: var(--clipHeight);
overflow: hidden;
flex-direction: column;
}
.text{
position: relative;
display: inline-flex;
align-items: center;
height: var(--clipHeight);
transition: all 0.8s cubic-bezier(0.625, 0.05, 0, 1);
transform: translateY(0);
}
.button:hover{
scale: 0.95;
}
.button:hover .text{
transform: translateY(calc(-1 * var(--clipHeight)));
}
Svelte implementation
svelte
<script>
let { text = "Hover over" } = $props();
</script>
<div class="button">
{#each text.split('') as letter, index}
<div class="clipContainer">
<div class="text" style="transition-delay: {index * 7}ms">
{letter === ' ' ? '\u00A0' : letter}
</div>
<div class="text" style="transition-delay: {index * 7}ms">
{letter === ' ' ? '\u00A0' : letter}
</div>
</div>
{/each}
</div>
<style>
.button {
font-family: 'Host Grotesk';
font-size: 1.33rem;
font-weight: 500;
background-color: white;
color: black;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
cursor: pointer;
display: inline-flex;
transition: all 0.8s cubic-bezier(0.625, 0.05, 0, 1);
/* box-shadow only for visibility on white backgrounds */
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
--clipHeight: 1.4em;
}
.clipContainer {
position: relative;
display: inline-flex;
height: var(--clipHeight);
overflow: hidden;
flex-direction: column;
}
.text {
position: relative;
display: inline-flex;
align-items: center;
height: var(--clipHeight);
transition: all 0.8s cubic-bezier(0.625, 0.05, 0, 1);
transform: translateY(0);
}
.button:hover {
scale: 0.95;
}
.button:hover .text {
transform: translateY(calc(-1 * var(--clipHeight)));
}
</style>
<script>
let { text = "Hover over" } = $props();
</script>
<div class="button">
{#each text.split('') as letter, index}
<div class="clipContainer">
<div class="text" style="transition-delay: {index * 7}ms">
{letter === ' ' ? '\u00A0' : letter}
</div>
<div class="text" style="transition-delay: {index * 7}ms">
{letter === ' ' ? '\u00A0' : letter}
</div>
</div>
{/each}
</div>
<style>
.button {
font-family: 'Host Grotesk';
font-size: 1.33rem;
font-weight: 500;
background-color: white;
color: black;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
cursor: pointer;
display: inline-flex;
transition: all 0.8s cubic-bezier(0.625, 0.05, 0, 1);
/* box-shadow only for visibility on white backgrounds */
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
--clipHeight: 1.4em;
}
.clipContainer {
position: relative;
display: inline-flex;
height: var(--clipHeight);
overflow: hidden;
flex-direction: column;
}
.text {
position: relative;
display: inline-flex;
align-items: center;
height: var(--clipHeight);
transition: all 0.8s cubic-bezier(0.625, 0.05, 0, 1);
transform: translateY(0);
}
.button:hover {
scale: 0.95;
}
.button:hover .text {
transform: translateY(calc(-1 * var(--clipHeight)));
}
</style>