Most React projects already have the structural UI covered: navigation, cards, grids, forms, responsive layouts.
The harder part is often the interaction layer — cursor effects, text reveals, scroll sequences, transitions, and other motion that sits between those components.
We can build those effects ourselves with GSAP, Motion, CSS, or WebGL. But when an interaction pattern already exists, starting from reusable source can save implementation time.
Hyperiux Vault provides creative effects for React and Next.js, including cursor interactions, text animations, scroll effects, navigation effects, loaders, page transitions, backgrounds, and WebGL scenes.
In this tutorial, we'll install Phantom Image Trail, a GSAP-powered cursor effect, into a Next.js project.
Then we'll:
- initialize Vault
- install the effect
- inspect the files added to the project
- render it on a real page
- customize the animation
- adjust its accessibility and mobile behavior
- verify everything with a production build
Version note: Vault is actively evolving, so record your CLI version before following the tutorial:
npx hyperiux --versionIf generated paths or implementation details differ from the examples below, use the files produced by your installed CLI as the source of truth.
How Hyperiux Vault works
Vault uses a source-first approach.
Instead of importing the visual component from a Vault runtime package, the CLI copies the selected effect's source files into your project.
You then import that local code from your application.
That means we can:
- inspect the animation implementation
- change GSAP timing and easing
- change styling
- modify responsive behavior
- adapt accessibility behavior
- maintain the source alongside the rest of the application
There is a tradeoff.
Once the implementation lives in our repository, accessibility, upgrades, compatibility, testing, and performance changes become our responsibility too.
We'll see a practical example of that later in this tutorial.
Prerequisites
The current Vault documentation supports setups including:
| Requirement | Supported setup |
|---|---|
| Node.js | 18.17+; Node 20+ recommended |
| React | React 18+ |
| Next.js | Next.js 14 or 15 |
| Tailwind CSS | v3 or v4 |
| Language | TypeScript recommended; modern JavaScript supported |
| Package managers | npm, pnpm, yarn, or bun |
| Project structure | App Router, Pages Router, or modern Vite React |
For this tutorial we'll use:
- Next.js App Router
- TypeScript
- Tailwind CSS
- npm
A simplified existing project might look like:
my-app/
├── src/
│ └── app/
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── public/
├── package.json
└── tsconfig.json
Using src/ is not required by Vault.
If your project doesn't use it, keep your existing structure.
Before changing an existing project
Vault writes source into our repository, so it is worth creating a clean checkpoint first.
If you're using Git:
git status --short
Commit your current work, or otherwise make sure you can restore the project.
Also record the Vault CLI version:
npx hyperiux --version
After installation we'll inspect the project diff rather than assuming which files changed.
Initialize Hyperiux Vault
From the project root, run:
npx hyperiux init
Vault initializes configuration for the project.
The current CLI structure uses a root-level configuration file named:
hyperiux.json
This configuration tells Vault how the project is structured and where different source files belong.
For example, an aliases section may look similar to:
{
"aliases": {
"components": "@/components",
"effects": "@/components/effects",
"hooks": "@/hooks",
"lib": "@/lib"
}
}
Open the hyperiux.json generated in your project rather than copying an example blindly.
The most important value for this tutorial is:
aliases.effects
That determines where Vault effects are written and helps explain the import path we'll use later.
Preview the Phantom Image Trail installation
Before installing the effect, you can inspect what the CLI plans to add:
npx hyperiux add phantom-image-trail --dry-run
This is particularly useful when you're integrating Vault into an existing codebase.
Now install the effect:
npx hyperiux add phantom-image-trail
phantom-image-trail is the Vault effect we'll use throughout this tutorial.
It uses GSAP for the animation.
After installation, verify that GSAP exists in the project:
npm ls gsap
If it is missing, install it:
npm install gsap
Browse available Vault effects
If you want to see the effects currently available through the CLI, run:
npx hyperiux list
This command shows all available Vault effects.
It is not a list of effects already installed in your application.
For installation verification, we'll instead use:
- the generated source files
- the project diff
- the resolved React import
- the running application
Those checks tell us much more about whether the integration actually works.
Inspect exactly what changed
After installing the effect, run:
git status --short
git diff
This is useful for any source-first tool.
Rather than assuming the CLI touched only one directory, we can inspect the actual changes made by the version we're using.
Pay attention to:
hyperiux.json- effect source files
package.json- lockfiles
- stylesheets
- any other modified project files
If something unexpected changed, investigate it before continuing.
What source did Vault add?
The exact generated filenames and directories can vary with project configuration and Vault versions.
For a TypeScript project using src/, the result may resemble:
my-app/
├── hyperiux.json
├── src/
│ ├── app/
│ │ └── page.tsx
│ └── components/
│ └── effects/
│ └── phantom-image-trail/
│ ├── index.tsx
│ ├── useMouse.ts
│ └── createSuspendedRaf.ts
└── package.json
Use the files your CLI actually generated.
The three main pieces have different responsibilities:
-
indexcontains the component and the GSAP trail animation -
useMousetracks pointer movement -
createSuspendedRafmanages animation-frame work
This is one of the useful parts of Vault's model: if we later want to change pointer behavior, spawning rules, or the animation lifecycle, we know where to look.
Add Phantom Image Trail to the page
Assuming your Vault effects alias resolves to @/components/effects, import the component like this:
import PhantomImageTrail from "@/components/effects/phantom-image-trail";
If your hyperiux.json uses another path, use that instead.
Now update src/app/page.tsx:
import PhantomImageTrail from "@/components/effects/phantom-image-trail";
const images = [
{
src: "https://pub-8abee449136941f5b0a1cd2c014534e9.r2.dev/vault-listing-images/assets-images/h-11.jpg",
alt: "",
},
{
src: "https://pub-8abee449136941f5b0a1cd2c014534e9.r2.dev/vault-listing-images/assets-images/h-12.jpg",
alt: "",
},
{
src: "https://pub-8abee449136941f5b0a1cd2c014534e9.r2.dev/vault-listing-images/assets-images/h-13.jpg",
alt: "",
},
];
export default function Home() {
return (
<main className="min-h-screen overflow-hidden bg-[#f8fdfe]">
<PhantomImageTrail
images={images}
enableRotation={true}
idleSpawn={false}
idleDelay={300}
cursorOffsetX={-12}
cursorOffsetY={-12}
popOutDuration={0.8}
fadeOutDuration={0.5}
idlePopOutMultiplier={2.2}
idleFadeMultiplier={1.8}
imageMultiplier={3}
/>
</main>
);
}
With:
idleSpawn={false}
we expect images to appear when the pointer moves rather than spawning while the cursor is idle.
The full-height wrapper is only demo framing. Phantom Image Trail does not require every production page to occupy exactly one viewport.
Fix decorative image semantics
For this example, the trail images are purely decorative.
Normally that would mean:
alt=""
But this is where inspecting the copied Vault source becomes useful.
In the current Phantom Image Trail implementation, the image normalization logic uses a fallback similar to:
alt: image.alt || `Trail ${index + 1}`,
An empty string is falsy, so our intentional:
alt: ""
gets replaced with something such as:
Trail 1
For decorative images, that's not what we want.
Open the generated Phantom Image Trail entry file.
Before
alt: image.alt || `Trail ${index + 1}`,
After
alt: image.alt ?? `Trail ${index + 1}`,
The nullish coalescing operator only falls back for null or undefined.
An intentionally empty string remains empty.
That means:
alt: ""
now correctly produces decorative image semantics.
If your images contain meaningful project or product information, use appropriate alt text instead — and make sure the same information exists independently of the cursor animation.
Run the application
Start Next.js:
npm run dev
Open the URL printed by Next.js, normally:
http://localhost:3000
Move the pointer across the page.
You should see images appear along the pointer path, animate into view, and fade away.
Verify:
- the component renders
- pointer movement produces the trail
- GSAP resolves correctly
- Tailwind styling is present
- there are no console errors
- there are no hydration warnings
A successful CLI command isn't enough. The useful verification is seeing the interaction work inside the real application.
Customize the images
Now let's replace the demo images.
Add:
public/
└── images/
├── project-01.jpg
├── project-02.jpg
└── project-03.jpg
Then update the array:
const images = [
{ src: "/images/project-01.jpg", alt: "" },
{ src: "/images/project-02.jpg", alt: "" },
{ src: "/images/project-03.jpg", alt: "" },
];
Because we already adjusted the source normalization, those empty strings remain decorative.
Reduce the trail intensity
Let's make the effect calmer.
Before
<PhantomImageTrail
images={images}
enableRotation={true}
popOutDuration={0.8}
fadeOutDuration={0.5}
imageMultiplier={3}
/>
After
<PhantomImageTrail
images={images}
enableRotation={false}
triggerDistance={140}
popOutDuration={0.55}
fadeOutDuration={0.35}
imageMultiplier={2}
/>
triggerDistance controls how far the pointer travels before another image is introduced.
imageMultiplier deserves extra attention.
The implementation effectively uses:
source image count × imageMultiplier
So three source images with:
imageMultiplier={3}
can result in nine trail image elements being available to the animation.
That means imageMultiplier affects performance as well as aesthetics.
Make mobile behavior intentional
Cursor effects are primarily designed around fine-pointer devices.
For production, we generally don't want a decorative desktop cursor trail to become required touch interaction.
The current implementation includes mobile-related behavior such as mobile detection and tap spawning.
The source currently exposes defaults equivalent to:
disableOnMobile = false,
enableMobileTap = true,
That means touch interaction may still spawn decorative trail images.
For this tutorial, we want coarse-pointer users to keep normal touch behavior.
Because these are implementation-level details rather than something we should assume is permanent public API, we'll change the owned source.
Update the defaults:
disableOnMobile = true,
enableMobileTap = false,
Then make the tap handler explicitly respect disableOnMobile.
Before
if (!enableMobileTap || !isMobileRef.current) return;
After
if (
disableOnMobile ||
!enableMobileTap ||
!isMobileRef.current
) {
return;
}
Also include disableOnMobile in that callback's dependency array where appropriate.
Now tapping on a touch-first device does not spawn decorative trail images.
Make reduced motion actually disable the trail
The effect source also detects:
(prefers-reduced-motion: reduce)
but detection alone isn't enough.
For our final implementation, users requesting reduced motion should not receive the pointer trail.
Find the function responsible for spawning the next image.
If it currently starts with something like:
if (!totalImages) return;
change it to:
if (prefersReducedMotion || !totalImages) return;
Make sure prefersReducedMotion appears in the callback's dependency list.
We can also stop idle spawning immediately:
if (
prefersReducedMotion ||
!idleSpawn ||
(disableOnMobile && isMobileRef.current)
) {
return;
}
And if your frame loop continues processing pointer animation work, give it an early return too:
if (prefersReducedMotion) {
return;
}
If the generated implementation contains UI copy saying that the trail continues under reduced motion, remove or update that message because it no longer reflects our behavior.
Our fallback is simple: the page remains usable without the decorative trail.
That's exactly what we want.
Why page.tsx doesn't need "use client"
Phantom Image Trail depends on browser-side behavior such as:
- pointer movement
window.matchMedia- animation frames
- measurements
- GSAP timelines
So the effect itself belongs behind a Client Component boundary.
The generated implementation already establishes that boundary with:
"use client";
Our page.tsx does not need the same directive.
A Next.js Server Component can render a Client Component.
The boundary begins at the client-marked module.
We also don't need to reach for:
dynamic(..., { ssr: false })
automatically.
Use that pattern when an implementation genuinely cannot participate in server rendering. Don't disable SSR by reflex.
Common problems
The import cannot be resolved
Open:
hyperiux.json
and inspect:
aliases.effects
Then compare it with the directory created by the CLI.
Start with the filesystem, not guesswork.
GSAP cannot be resolved
Run:
npm ls gsap
If GSAP is absent:
npm install gsap
Styles are missing
Vault supports Tailwind CSS v3 and v4.
For Tailwind v3, make sure your content configuration scans the directory where the effect was generated.
Tailwind v4 uses a different source-detection model, so debug the actual v4 setup rather than blindly copying a v3 configuration.
My screen reader announces “Trail 1”
Check the image normalization change.
For decorative images, explicit:
alt: ""
must survive normalization.
Images still spawn when I tap on mobile
Inspect the generated mobile tap handler.
Make sure:
enableMobileTap
is disabled and that your handler also respects:
disableOnMobile
Reduced motion still shows the trail
Confirm that prefersReducedMotion prevents the image-spawning function itself from running.
Showing a reduced-motion notice while continuing the same animation is not the behavior we're targeting.
Performance dropped after replacing the images
Animation cost is only part of the equation.
Large photography also adds:
- network transfer
- image decoding
- memory usage
- compositing work
Try lowering:
imageMultiplier
and optimize the image files before assuming the GSAP timeline itself is the bottleneck.
Optional CLI troubleshooting
If you want to inspect which commands your installed Vault CLI exposes:
npx hyperiux --help
The Vault documentation also references:
npx hyperiux doctor
for diagnosing project configuration issues.
Because CLI capabilities can change by version, checking --help first keeps the tutorial tied to the software actually installed in your project.
Verify the production build
Development mode isn't the final test for a Next.js animation.
Build the project:
npm run build
This can expose problems such as:
- TypeScript failures
- unresolved imports
- build-time rendering errors
- dependency problems
- incorrect client/server boundaries
If your project uses the standard Next.js production script:
npm run start
Test that build instead of judging performance only from npm run dev.
Production checklist
Before shipping Phantom Image Trail:
- [ ]
npm run buildsucceeds - [ ] the trail works with a fine pointer on desktop
- [ ] coarse-pointer devices keep normal touch behavior
- [ ] tapping on mobile does not spawn decorative trail images
- [ ]
prefers-reduced-motion: reduceprevents the trail from spawning - [ ] decorative images preserve
alt="" - [ ] meaningful content exists independently of the cursor animation
- [ ] focus rings and clickable controls remain unobstructed
- [ ] there are no console or hydration warnings
- [ ] images are appropriately compressed
- [ ] performance is tested using the production build
- [ ] the post-install Git diff contains only changes you intend to keep
Final result
We didn't stop at:
npx hyperiux add phantom-image-trail
We initialized Vault, inspected hyperiux.json, previewed the installation, checked the resulting source files, rendered the GSAP image trail in Next.js, customized it, and changed the copied implementation so its decorative images, mobile behavior, and reduced-motion behavior match the production expectations we're testing against.
That's the practical consequence of source-first components.
The install command gives us a starting point. The code is ours after that.
Browse Hyperiux Vault and the Vault documentation, find an interaction that solves a real design problem, and inspect what it actually does once it enters your repository.
The interesting part starts after the install command.
Top comments (0)