Dev.to WebDev 🛠 Dev 👁 0 📖 15 min read

How to Add Award-Winning Website Interactions to React Without Building Every Effect From Scratch

How to Add Award-Winning Website Interactions to React Without Building Every Effect From Scratch A surprising number of “award-site” interactions are difficult for reasons that have little to do with the visible trans

How to Add Award-Winning Website Interactions to React Without Building Every Effect From Scratch

A surprising number of “award-site” interactions are difficult for reasons that have little to do with the visible transform.

Moving a heading upward is easy. The real work appears around it: deciding when the sequence starts, coordinating it with nearby elements, handling resize, cleaning up listeners or timelines, adapting pointer behaviour for touch, respecting reduced motion, and making sure a Next.js route does not inherit animation state from a component that no longer exists.

Building all of that from scratch for every section is rarely a good use of creative frontend time.

A more practical approach is to separate three decisions:

  1. Use CSS for interaction that does not need application state or continuous measurement.
  2. Use an animation or rendering tool when the behaviour actually requires one.
  3. Reuse existing interaction machinery when the interesting part of the work is adaptation, not invention.

Then spend bespoke engineering on the moments that make the site recognisably yours.

Start with the job of the interaction

Before choosing GSAP, Motion, Three.js or anything else, decide what changes for the user when the interface moves.

A useful interaction might acknowledge input, control reading order, preserve context while evidence changes, show how two states relate, or create one deliberately high-attention brand moment.

Those are different jobs, and they deserve different levels of technical effort.

A practical way to think about them is:

Interaction level Typical job Examples
Functional Explain state or acknowledge input Button feedback, tabs, accordion, modal
Narrative Control how content unfolds Text reveal, sticky feature story, image transition
Signature Create a distinctive product or brand moment Spatial gallery, unusual hero, generative canvas, WebGL treatment

Most pages need many functional interactions, a smaller number of narrative ones, and perhaps one signature interaction.

The imbalance usually appears when everything is promoted to the third category. The eyebrow fades, the heading splits into words, the cards stagger, the screenshot scales, the background drifts and the cursor changes shape inside the same viewport. Nothing is technically broken. Motion has simply removed the hierarchy.

Reuse the interaction primitive, not the finished design

Suppose a designer gives you this feature section:

Keep the product screenshot pinned while three claims move through the left column. Each claim should bring a different part of the screenshot into focus.

The distinctive part is the relationship between the claims and the product.

A large amount of the implementation around that idea is less distinctive:

  • measuring the section;
  • deciding when it becomes active;
  • tracking progress;
  • coordinating the sticky state;
  • cleaning up;
  • dealing with resizing;
  • reducing the behaviour on smaller screens;
  • providing a readable reduced-motion state.

You can build all of that again. You can also start with a working sticky/scroll interaction and spend more time deciding how this particular product should behave inside it.

This is where reusable interaction primitives become useful.

A primitive is not a finished aesthetic. It supplies enough behaviour to avoid rebuilding the same machinery while leaving the design decisions exposed.

For example:

<Reveal
  trigger="viewport"
  distance={24}
  duration={0.7}
>
  <FeatureGrid />
</Reveal>

or:

<ScrollSequence>
  <StickyMedia />
  <FeatureSteps />
</ScrollSequence>

Your project still decides the typography, hierarchy, timing, visual treatment, breakpoints, content and fallback.

The reusable part is infrastructure. The authorship is in what you do with it.

Use CSS when CSS already solves the problem

Creative frontend becomes unnecessarily complicated when JavaScript is treated as the entrance fee for interesting motion.

CSS is often enough for:

  • hover feedback;
  • button fills;
  • simple transforms;
  • masks and clipping;
  • decorative loops;
  • focus states;
  • uncomplicated entrances;
  • colour and underline transitions.

A button fill, for example, needs very little machinery:

<button className="fillButton">
  <span>View project</span>
</button>
.fillButton {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  border: 1px solid currentColor;
  padding: 0.75rem 1rem;
  background: transparent;
  color: #111;
  cursor: pointer;
}

.fillButton::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: #111;
  transform: translateY(101%);
  transition: transform 500ms cubic-bezier(.22, 1, .36, 1);
}

.fillButton > span {
  position: relative;
}

.fillButton:hover::before,
.fillButton:focus-visible::before {
  transform: translateY(0);
}

.fillButton:hover,
.fillButton:focus-visible {
  color: #fff;
}

@media (prefers-reduced-motion: reduce) {
  .fillButton::before {
    transition-duration: 0.01ms;
  }
}

There is no timeline to initialise, no component state to synchronise and no resize work.

The :focus-visible state also matters. An interaction that communicates affordance only when a mouse happens to be above it has confused an input method with a user requirement.

Use Motion when animation belongs naturally to component state, layout or gestures

Motion fits particularly well when movement is already close to the React component model: entering and leaving UI, layout changes, drag or press states, viewport-triggered reveals, and scroll-linked values.

A reusable reveal can stay fairly small:

"use client";

import { motion, useReducedMotion } from "motion/react";
import type { ReactNode } from "react";

type RevealProps = {
  children: ReactNode;
  delay?: number;
};

export function Reveal({
  children,
  delay = 0,
}: RevealProps) {
  const shouldReduceMotion = useReducedMotion();

  return (
    <motion.div
      initial={
        shouldReduceMotion
          ? { opacity: 1 }
          : { opacity: 0, y: 24 }
      }
      whileInView={{
        opacity: 1,
        y: 0,
      }}
      viewport={{
        once: true,
        amount: 0.25,
      }}
      transition={{
        duration: shouldReduceMotion ? 0 : 0.7,
        delay: shouldReduceMotion ? 0 : delay,
        ease: [0.22, 1, 0.36, 1],
      }}
    >
      {children}
    </motion.div>
  );
}

Motion currently supports viewport-triggered behaviour through whileInView, scroll-linked values through APIs such as useScroll, layout animation, and gesture states including hover, tap and focus.

The code above is deliberately modest. That is useful.

A generic reveal should not become the visual protagonist of the page. Its job is to establish reading order and then get out of the way.

Use GSAP when choreography becomes the problem

Some interactions stop being individual state changes and become sequences.

The title arrives. The supporting copy overlaps its final 300 milliseconds. The media starts slightly before the copy finishes. A later section pins to the viewport, advances with scroll progress, and needs to revert correctly when the component unmounts.

Timeline-oriented control is valuable here.

With React, lifecycle matters as much as sequencing. The official @gsap/react integration provides useGSAP(), scoped contexts and cleanup behaviour; GSAP's current guidance also recommends registering the hook as a plugin.

"use client";

import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";

gsap.registerPlugin(useGSAP);

export function Hero() {
  const root = useRef<HTMLElement>(null);

  useGSAP(
    () => {
      const timeline = gsap.timeline();

      timeline
        .from(".hero-title", {
          yPercent: 100,
          duration: 0.8,
          ease: "power3.out",
        })
        .from(
          ".hero-copy",
          {
            opacity: 0,
            y: 24,
            duration: 0.6,
            ease: "power2.out",
          },
          "-=0.35"
        );
    },
    { scope: root }
  );

  return (
    <section ref={root}>
      <div className="hero-title-mask">
        <h1 className="hero-title">
          Build the memorable part.
        </h1>
      </div>

      <p className="hero-copy">
        Reuse the machinery around it.
      </p>
    </section>
  );
}
.hero-title-mask {
  overflow: hidden;
}

Scoping the timeline to root prevents those class selectors from becoming accidental global animation targets. useGSAP() also records GSAP work in its context so it can be reverted when the component is torn down.

If an event handler creates additional GSAP work later, treat that lifecycle separately rather than assuming everything created after the initial hook is automatically covered.

In Next.js, decide the client boundary before the animation API

A motion-heavy component often depends on things the server cannot provide: pointer coordinates, window, layout measurements, canvas, WebGL or browser observers.

That is an architectural condition, not a reason to turn an entire page into a client component.

Keep the static content and layout server-rendered where it makes sense, then isolate the interaction behind the smallest useful client boundary:

// app/product/page.tsx
import { InteractiveHero } from "@/components/interactive-hero";

export default function ProductPage() {
  return (
    <main>
      <InteractiveHero />
      <ProductDetails />
    </main>
  );
}
// components/interactive-hero.tsx
"use client";

export function InteractiveHero() {
  // Browser-dependent animation logic lives here.
  return <section>{/* ... */}</section>;
}

For particularly browser-bound experiences such as WebGL scenes, a dynamic client-only import can also be appropriate when server rendering provides no useful output.

The exact boundary depends on the effect. The useful rule is narrower: do not widen the client surface merely because one part of a section moves.

This also gives hydration failures fewer places to hide.

Vault's current installation guidance applies the same principle to effects that depend on scroll position, pointer movement, layout measurement, animation timelines, canvas or WebGL.

Treat WebGL as an escalation

Three.js and React Three Fiber are justified when the visual model needs something the DOM is poor at producing: depth, custom shaders, particle systems, image displacement, lighting, spatial scenes or other GPU-driven rendering.

They are less convincing when the requirement is “make the hero feel expensive.”

As a complexity heuristic, start with the simplest rendering layer that can express the idea. A DOM treatment may be enough. Canvas becomes useful for different workloads. WebGL earns its place when the experience needs the rendering model it provides.

The important production questions also change once a scene renders continuously:

  • What happens to the render loop when the scene is off-screen?
  • How high can the device pixel ratio go?
  • What changes on a weaker phone?
  • How expensive are textures, shaders and post-processing passes?
  • Does reduced motion remove movement or provide a static alternative?

A WebGL scene can be technically impressive and still be the wrong interaction for a pricing page whose main job is comparison.

One section, three implementation decisions

Consider a common product-story section:

Desktop behaviour

A product image remains pinned on the right. Three claims move through the left column. As each claim becomes active, the image changes emphasis to match it.

There are at least three different pieces of interaction inside that one idea.

1. The claim entrance

A subtle opacity/position reveal does not need a custom timeline. Motion's viewport behaviour or even CSS plus an observer is enough.

2. The pinned claim-to-image sequence

This is where the state relationship matters. The active claim must stay synchronised with the media, and the section may need scroll progress or a timeline. GSAP/ScrollTrigger, Motion scroll values or an existing scroll primitive can all be reasonable depending on the exact choreography.

The choice should follow the behaviour, not a rule that “award sites use GSAP.”

3. The mobile version

A 390px-wide screen does not need a miniature sticky desktop composition.

Stack the claim beside its corresponding image. Preserve the claim/evidence relationship and remove the mechanism that was only useful because desktop had room for two persistent columns.

The reduced-motion version can make the same move. Show each claim with its evidence in a stable reading sequence rather than forcing the user through a scrubbed transition.

That is the larger pattern: reuse does not remove design decisions. It moves your engineering time toward the decisions that survive across contexts.

Source-first interaction components make more sense once the demo stops matching the product

A reusable interaction becomes much more interesting when its implementation is editable.

The demo's breakpoint will eventually be wrong for your layout.

Its easing may feel too soft beside the rest of your interface.

The hero might sit inside a different stacking context. Your typeface may wrap the animated heading into three lines instead of two. A pointer effect that looks good with a mouse may need an entirely different touch treatment.

At that point, an API with twenty configuration props and source code you can directly modify are different kinds of convenience.

Source-first interaction components let the effect become normal project code:

components/
  interactions/
    sticky-feature/
      StickyFeature.tsx
      useStickyFeature.ts
      styles.css

You can inspect the listener setup, change the DOM structure, alter a breakpoint, simplify the mobile path, remove a dependency or replace part of the implementation when the effect allows it.

The abstraction gets you to a working behaviour. It does not have to remain intact forever.

This is the model behind Hyperiux Vault

This is also the workflow we are building around Hyperiux Vault.

Vault is a library of creative interaction patterns for React and Next.js. The current catalogue spans areas including text animation, backgrounds, buttons, carousels, scroll interactions, navigation, cursor effects, loaders, page transitions and WebGL. Different effects can use different tools rather than forcing the entire catalogue through one animation engine.

The important part for this argument is how the effects enter the project.

Vault uses a source-first workflow: the selected effect is added as editable project files, with effect-specific dependencies made visible rather than hidden behind one large interaction runtime.

The expected workflow is:

  1. Preview a behaviour that solves the interaction job.
  2. Add the relevant source.
  3. Read the implementation.
  4. Remove or alter what your page does not need.
  5. Tune the behaviour until it belongs to the product.
  6. Test it under the conditions the original demo could not know about.

That last part is where most of the real work lives.

A reusable interaction should save you from rebuilding solved machinery. It should not decide the art direction for you.

Customise the variables people actually perceive

Changing the accent colour does not make a reused interaction feel native to the site.

The more meaningful variables are usually behavioural.

Timing and easing change weight. A short response with a direct easing curve feels different from a long, heavily eased entrance even when both travel the same distance.

Distance and scale change visual force. Moving 12 pixels establishes hierarchy. Moving an entire viewport turns the transition into an event.

Trigger position changes pacing. An entrance that starts as soon as an element touches the viewport has a different reading rhythm from one that waits until the section is established.

Typography changes animation geometry. A split-text effect that looked clean in the demo can fall apart once another font, line height or responsive measure creates different wrapping.

Input and breakpoint behaviour decide whether the interaction still has a job. Pointer attraction does not become a mobile interaction simply because the viewport is narrower.

These choices are good candidates for shared project-level motion values:

export const motionTokens = {
  duration: {
    quick: 0.2,
    standard: 0.5,
    expressive: 0.9,
  },
  easing: {
    standard: [0.22, 1, 0.36, 1],
    soft: [0.16, 1, 0.3, 1],
  },
  distance: {
    subtle: 12,
    standard: 24,
    large: 48,
  },
} as const;

Not every effect should use the same duration. The point is to create a vocabulary narrow enough that unrelated interactions still appear to belong to the same product.

Performance belongs in the interaction decision

Creative frontend work becomes difficult to optimise when performance review begins after the interaction is already considered finished.

Ask about cost while choosing the behaviour.

A DOM reveal that changes transform and opacity has a very different workload from a scroll handler that repeatedly measures layout. A cursor effect that creates work for every pointer event deserves different scrutiny from a CSS hover transition. A WebGL scene that renders every frame keeps spending resources even when nothing obvious changes.

Useful production questions include:

  • Does anything continue running off-screen?
  • Are scroll and pointer listeners cleaned up?
  • Is layout being read repeatedly during animation?
  • Can event work be coalesced into the render loop rather than triggering uncontrolled updates?
  • Can mobile reduce the amount of work rather than merely shrink the layout?
  • Does a continuously rendered canvas need the device's full pixel ratio?
  • Can decorative assets or heavy rendering load after critical content?
  • What remains when animation is removed?

There is no universal rule that the least visually complex effect is always the cheapest. Measure the implementation you actually ship.

Reduced motion is one requirement, not the accessibility strategy

Respecting prefers-reduced-motion is part of responsible motion design, and the browser exposes that preference specifically so interfaces can reduce or replace non-essential motion. It does not solve every accessibility problem by itself.

A production interaction also needs questions such as:

  • Can the control be reached with a keyboard?
  • Is the focus state visible?
  • Does a hover effect have another input path?
  • Does removing animation hide information or make state changes ambiguous?
  • Is the static DOM still semantic?
  • Can touch users perform the action without precise pointer coordinates?
  • If content enters through animation, is it still readable when the animation system fails?

For a cursor trail, reduced motion may mean disabling the trail. For a route transition, it may mean using a near-instant state change. For a product explanation whose animation carries meaning, it may require a different static layout rather than simply setting every duration to zero.

Accessibility changes the design of the fallback, not just the speed of the tween.

Do not animate every element that enters the viewport

The most reusable animation pattern in frontend development may also be the easiest to overuse:

element enters viewport
↓
opacity 0 → 1
translateY 24px → 0

Apply that to a few important transitions and it can help establish order.

Apply it to every heading, paragraph, card, logo and footer link and the page starts acknowledging the existence of its own DOM.

A better page has contrast.

The hero can carry a more expressive entrance. Narrative sections can reveal content where sequence matters. Dense product information can remain stable. Buttons can respond quickly to input. One signature section can consume more of the interaction budget.

And the footer can remain a footer.

Stillness is part of the motion system.

Build from scratch where originality has leverage

Reusable interaction primitives should not replace custom creative development.

Build the interaction specifically for the project when the behaviour itself carries the idea: a product needs an unusual manipulation model, the brand concept depends on custom rendering, existing primitives keep fighting the desired composition, or the interaction will become a recognisable part of the product.

Those are good places to spend bespoke engineering time.

Rebuilding yet another viewport observer, split-text wrapper or basic magnetic button has less leverage if a maintainable implementation already exists and can be adapted.

This is the trade-off worth protecting:

reuse the solved infrastructure so custom effort can go somewhere users will actually notice.

A practical workflow

When adding high-end interaction to an existing React or Next.js site, I would work in roughly this order:

  1. Make the static design work first. Motion will not repair weak hierarchy, spacing or typography.
  2. Identify the high-attention moments. The hero, product explanation or major transition usually deserves more budget than supporting content.
  3. Write down each interaction's job. “Connect these two product states” leads to better implementation decisions than “add animation.”
  4. Look for an existing primitive before building infrastructure.
  5. Inspect the implementation before adopting it. Check dependencies, listeners, observers, lifecycle, rendering method and browser assumptions.
  6. Choose the client boundary deliberately in Next.js.
  7. Remove capability the page does not need.
  8. Apply the project's timing, spacing, type and breakpoint logic.
  9. Design touch, keyboard and reduced-motion behaviour rather than treating them as post-processing.
  10. Test the composition on real devices.
  11. Remove the weakest effect.

The last step sounds flippant until you try it.

Two individually excellent interactions can compete when they share the same page. Removing one often gives the other enough visual space to become the memorable moment.

A better test for “custom”

Developers sometimes use custom as a statement about where the code originated.

Users cannot see your Git history.

They experience whether the response feels immediate, whether the sequence is easy to follow, whether movement belongs to the visual language, and whether the interaction survives the device they happen to be using.

A bespoke experience can start from bespoke code.

It can also start from an existing interaction whose timing, structure, input behaviour and fallback have been changed until they fit the product around it.

The test is not whether every line began with an empty file.

It is whether the interaction still looks borrowed when the work is finished.

Final thought

Award-winning website interaction does not require turning every React page into an animation research project.

Use CSS when the browser already has the mechanism. Use Motion when component state, gestures, layout or scroll fit its model. Use GSAP when sequencing and choreography demand tighter control. Escalate to Canvas or WebGL when the visual idea requires another rendering layer. Reuse interaction source where rebuilding the surrounding machinery adds little creative value.

Then spend the saved effort on the parts where judgement still matters: pacing, hierarchy, fallback behaviour, responsive adaptation and the one moment the page should genuinely own.

The page does not need to move more.

It needs better reasons to move.

I work on Hyperiux Vault, where we're building source-first interaction patterns for React and Next.js. The product is based on the same premise described here: start from editable interaction machinery, then adapt it until it belongs to the project rather than the demo.

For creative React work, where do you draw the line between a reusable interaction primitive and something worth building from scratch?

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.