Linear - Hero section

I really liked the hero animation on Linear’s website. The content fades in one element at a time, and the sequence feels clean and intentional. To understand how it worked, I recorded the animation, went through it frame by frame, and inspected elements.

Linear uses a combination of opacity, blur, and translateY offset. Opacity gives the fade-in, blur makes the entry look softer, and translateY creates the upward motion. All three together create the effect.


To recreate it, keyframe animation was the simplest approach. The entire hero section uses variations of the same motion pattern, so defining one animation and reusing it across elements kept things consistent.

@keyframes reveal {
  from { 
    transform: translateY(50%);
    opacity: 0; 
    filter: blur(10px)
  }
  to { 
    transform: translateY(0); 
    opacity: 1;
    filter: blur(0px);
  }
}

The headline needed the most setup. To get each word to animate individually, I split the sentence into separate spans and applied staggered delays. Without doing this, the full line animates as a block, which isn’t how Linear’s version behaves.

export default function HeroSection() {
  const TITLE =
    "Linear is a purpose-built tool for planning and building products";

  const words = TITLE.split(" ");

  return (
    <div className={`flex flex-col h-[480px] relative ${inter.variable} ${styles.heroFont}`}>
        <div className="flex flex-col flex-1 px-16 justify-center">
            {/* Title */}
            <div className="pb-3">
                <h1 key={iteration} className={styles.title}>
                {words.map((word, index) => (
                    <React.Fragment key={index}>
                    <span style={{ "--index": `${index}` } as CSSVar}>{word}</span>
                    {index < words.length - 1 && " "}
                    </React.Fragment>
                ))}
                </h1>
            </div>
        </div>
    </div>
  );

.title span {
  display: inline-block;
  animation: reveal 4s cubic-bezier(0.19, 1, 0.22, 1) both;
  animation-delay: calc(var(--index) * 0.03s);
}

The paragraph and the button group animate as whole units. I added a delay so they appear only after the headline finishes, which matches the pacing on the original site.

{/* Sub-title */}
<div className="pb-7">
    <p key={iteration} className={styles.subTitle}>
        Meet the system for modern software development. Streamline issues, projects, and product roadmaps.
    </p>
</div>

{/* Button group (product CTAs) */}
<div key={iteration} className={styles.buttonGroup}>
    {/* Start building */}
    <button className="bg-white text-black rounded-md px-3 py-2 text-xs font-medium shadow-sm hover:bg-neutral-100 transition">
        Start building
    </button>

    {/* New: Linear agent for Slack → */}
    <button className="flex items-center gap-1 text-xs font-medium text-gray-200 hover:text-neutral-100 transition">
        <span className="font-medium">New:</span>
        <span>Linear agent for Slack</span>
        <span className="text-gray-200">
            <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="#e5e7eb" viewBox="0 0 256 256">
                    <path d="M184.49,136.49l-80,80a12,12,0,0,1-17-17L159,128,87.51,56.49a12,12,0,1,1,17-17l80,80A12,12,0,0,1,184.49,136.49Z"></path>
            </svg>
        </span>
    </button>
</div>
.subTitle {
    max-width: 80%;
    color: #8a8f98;

    display: inline-block;
    animation: reveal 2s cubic-bezier(0.19, 1, 0.22, 1) both;
    animation-delay: 0.5s;
}

.buttonGroup {
    display: flex;
    align-items: center;
    gap: 16px;

    animation: reveal 2s cubic-bezier(0.19, 1, 0.22, 1) both;
    animation-delay: 0.5s;
}

Recreating this was a good exercise. You can pick up a lot just by taking things apart and checking what’s actually happening.

If you’d like to see more of my work, I share updates regularly on X (Twitter).