00:00:00
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>
000 views
Published 16.02.2026