Commerce
Search Ad
A search types out, the results show up, and the sponsored result is highlighted.
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(useGSAP);
export type SearchAdProps = {
/** Text typed into the search box. */
query?: string;
/** Domain shown on the sponsored result. */
adDomain?: string;
/** Title of the sponsored result. */
adTitle?: string;
/** Description of the sponsored result. */
adText?: string;
/** Domain of the organic result. */
organicDomain?: string;
/** Title of the organic result. */
organicTitle?: string;
/** Description of the organic result. */
organicText?: string;
/** Button shown under the results. */
cta?: string;
/** Color of the highlight and the button. */
accent?: string;
/** Replay forever with a pause between runs. */
loop?: boolean;
className?: string;
};
export default function SearchAd({
query = "running shoes",
adDomain = "example.com",
adTitle = "Brand · Running shoes on sale",
adText = "Free shipping over $50. Easy returns.",
organicDomain = "reviews.example",
organicTitle = "The best running shoes of the year",
organicText = "We compared the top picks by comfort and price.",
cta = "Visit store",
accent = "#1f5bff",
loop = true,
className = "",
}: SearchAdProps) {
const root = useRef<HTMLDivElement>(null);
const typed = useRef<HTMLSpanElement>(null);
useGSAP(
() => {
const q = gsap.utils.selector(root);
const counter = { n: 0 };
const write = () => {
if (typed.current) typed.current.textContent = query.slice(0, Math.round(counter.n));
};
const tl = gsap.timeline({ repeat: loop ? -1 : 0, repeatDelay: 1.4 });
tl.set(q("[data-result]"), { autoAlpha: 0, y: 12 })
.set(q("[data-cta]"), { autoAlpha: 0, y: 8 })
.set(counter, { n: 0 })
.add(() => {
if (typed.current) typed.current.textContent = "";
}, 0)
.to(q("[data-caret]"), { opacity: 0, duration: 0.3, repeat: 3, yoyo: true }, 0)
.to(counter, { n: query.length, duration: 1.3, ease: "none", onUpdate: write }, 0.4)
.to(q("[data-result]"), { autoAlpha: (i: number) => (i ? 0.55 : 1), y: 0, duration: 0.4, stagger: 0.15 }, 2.0)
.fromTo(q("[data-ad]"), { scale: 1 }, { scale: 1.03, duration: 0.25, yoyo: true, repeat: 1 }, 2.7)
.to(q("[data-cta]"), { autoAlpha: 1, y: 0, duration: 0.35 }, 3.2)
.to({}, { duration: 1.6 });
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) tl.progress(1).pause();
},
{ scope: root, dependencies: [query, loop] },
);
return (
<div
ref={root}
className={`w-full max-w-lg rounded-xl border border-neutral-200 bg-white p-5 text-neutral-900 shadow-xl ${className}`}
>
<div className="flex h-11 items-center gap-2 rounded-full border border-neutral-300 px-4 text-[15px]">
<span ref={typed} className="min-w-0 flex-1 truncate" />
<span data-caret className="inline-block h-5 w-px shrink-0 bg-neutral-900" />
</div>
<div className="mt-4 flex flex-col gap-3">
<div
data-result
data-ad
className="rounded-lg bg-white p-3 outline-2 -outline-offset-2 outline-solid"
style={{ outlineColor: accent }}
>
<div className="text-[11px] text-neutral-500">
<b className="font-bold text-neutral-900">Sponsored</b> · {adDomain}
</div>
<div className="mt-0.5 text-[15px] font-medium" style={{ color: accent }}>
{adTitle}
</div>
<p className="text-[12px] text-neutral-600">{adText}</p>
</div>
<div data-result className="rounded-lg bg-neutral-50 p-3">
<div className="text-[11px] text-neutral-500">{organicDomain}</div>
<div className="mt-0.5 text-[15px] font-medium text-blue-800">{organicTitle}</div>
<p className="text-[12px] text-neutral-600">{organicText}</p>
</div>
</div>
<span
data-cta
className="mt-4 inline-block rounded-md px-3 py-2 text-xs font-semibold text-white"
style={{ backgroundColor: accent }}
>
{cta}
</span>
</div>
);
}
Paste this into Claude Code inside your project. It installs the skills your agent needs, asks for your texts and colors, and recreates this animation in your stack.
SkillsGSAP skillsHyperFrames · optional, for video
Claude Code
Recreate the "Search Ad" animated component from Motionshelf (https://motionshelf-pi.vercel.app) in my current project, adapted to my brand. Answer me in the language I write to you in. Work in this order: 1. Tools: Check whether the official GSAP skills are installed (gsap-core, gsap-timeline, gsap-react). If they are not, ask me and then run `npx skills add https://github.com/greensock/gsap-skills` (choose Claude Code). If they do not show up in this session yet, read the SKILL.md files the installer created for gsap-timeline and gsap-react before writing code. 2. Project: Look at my project: framework (Next.js, Vite, plain HTML…), TypeScript or JavaScript, Tailwind or plain CSS. Build it as one component that fits this stack. In React use gsap and @gsap/react; in plain HTML use GSAP from npm or a CDN. 3. Ask me before coding: Ask me these in a single message, showing the example values as defaults: - the search query - the sponsored result (domain, title, description) and the organic one - the button label and the accent color - the language of the texts 4. The animation What it looks like: A search page: a rounded search box, a sponsored result with a colored outline, an organic result below it, and a button. Step by step: - The caret blinks, then at 0.4 s the query types itself out in 1.3 s. - At 2.0 s the results rise in one after another; the organic result stays at 55% opacity so the sponsored one stands out. - At 2.7 s the sponsored result pulses once (scale 1.03 and back); at 3.2 s the button appears. Rules: - Use one GSAP timeline that loops forever with a short pause between runs (repeat: -1). If the step by step gives the pause, use that value; otherwise repeatDelay about 1.4 s. - Set every starting state with set() at the start of the timeline, never with from(): on the second loop from() starts from the wrong place. - Elements that appear later and would push the layout (new chat bubbles, new rows) stay display:none until their turn; elements that already have their place in the layout (a subtitle, a button) start hidden with autoAlpha 0, so nothing jumps. - If there is a cursor or pointer, it aims at the measured center of its target (getBoundingClientRect), never at fixed pixel coordinates; if the component is scaled, divide by the scale. - Blur only in the middle of a movement and always end at blur(0). - With prefers-reduced-motion, jump to the final frame and stop (tl.progress(1).pause()). - In React, use useGSAP from @gsap/react with a scope ref, so the timeline is cleaned up on unmount. - All example content stays generic (no real brands, logos or people). - Keep it a generic search page; do not reproduce the logo or exact layout of a real search engine. 5. Check: Run it and watch the first and the third loop (they must look the same). Check it at 390 px wide with no sideways scroll, with reduced motion turned on, and with a clean console. Tell me what you checked. 6. Video (optional): If I also want this as an MP4 (ads, stories, WhatsApp status), install the HyperFrames plugin with `claude plugin marketplace add heygen-com/hyperframes` and `claude plugin install hyperframes@hyperframes`, then use /hyperframes:hyperframes to render the same animation in 9:16, 1:1 or 16:9. Rendering needs FFmpeg installed, or render with --docker (downloads about 3.3 GB the first time). Ask me before installing. Shortcut: If I only want the ready-made component instead, it installs with `npx shadcn@latest add https://motionshelf-pi.vercel.app/r/search-ad.json`.
Other agentsUsing Cursor, Codex, Gemini CLI or another agent? This version tells it to find its own way to load the same skills before it starts.
Other agents
Recreate the "Search Ad" animated component from Motionshelf (https://motionshelf-pi.vercel.app) in my current project, adapted to my brand. Answer me in the language I write to you in. Work in this order: 1. Tools: These instructions were written for Claude Code, which uses "skills" (folders with a SKILL.md that teach it a technique). First find out how you load skills, rules or instructions, and get the same knowledge: (a) if your agent is supported by the skills installer, ask me and run `npx skills add https://github.com/greensock/gsap-skills --agent <your-agent>` (for example codex, opencode, antigravity); (b) otherwise copy the skill folders from https://github.com/greensock/gsap-skills into your skills or rules directory (in Cursor: Settings → Rules → Remote Rule (GitHub) → greensock/gsap-skills); (c) if you cannot load skills at all, open the SKILL.md files in https://github.com/greensock/gsap-skills and read them as instructions before coding. Tell me which path you used. 2. Project: Look at my project: framework (Next.js, Vite, plain HTML…), TypeScript or JavaScript, Tailwind or plain CSS. Build it as one component that fits this stack. In React use gsap and @gsap/react; in plain HTML use GSAP from npm or a CDN. 3. Ask me before coding: Ask me these in a single message, showing the example values as defaults: - the search query - the sponsored result (domain, title, description) and the organic one - the button label and the accent color - the language of the texts 4. The animation What it looks like: A search page: a rounded search box, a sponsored result with a colored outline, an organic result below it, and a button. Step by step: - The caret blinks, then at 0.4 s the query types itself out in 1.3 s. - At 2.0 s the results rise in one after another; the organic result stays at 55% opacity so the sponsored one stands out. - At 2.7 s the sponsored result pulses once (scale 1.03 and back); at 3.2 s the button appears. Rules: - Use one GSAP timeline that loops forever with a short pause between runs (repeat: -1). If the step by step gives the pause, use that value; otherwise repeatDelay about 1.4 s. - Set every starting state with set() at the start of the timeline, never with from(): on the second loop from() starts from the wrong place. - Elements that appear later and would push the layout (new chat bubbles, new rows) stay display:none until their turn; elements that already have their place in the layout (a subtitle, a button) start hidden with autoAlpha 0, so nothing jumps. - If there is a cursor or pointer, it aims at the measured center of its target (getBoundingClientRect), never at fixed pixel coordinates; if the component is scaled, divide by the scale. - Blur only in the middle of a movement and always end at blur(0). - With prefers-reduced-motion, jump to the final frame and stop (tl.progress(1).pause()). - In React, use useGSAP from @gsap/react with a scope ref, so the timeline is cleaned up on unmount. - All example content stays generic (no real brands, logos or people). - Keep it a generic search page; do not reproduce the logo or exact layout of a real search engine. 5. Check: Run it and watch the first and the third loop (they must look the same). Check it at 390 px wide with no sideways scroll, with reduced motion turned on, and with a clean console. Tell me what you checked. 6. Video (optional): If I also want this as an MP4 (ads, stories, WhatsApp status), install the HyperFrames skills with `npx skills add heygen-com/hyperframes` (choose your agent) and render the same animation in 9:16, 1:1 or 16:9. Rendering needs FFmpeg installed, or render with --docker (downloads about 3.3 GB the first time). Ask me before installing. Shortcut: If I only want the ready-made component instead, it installs with `npx shadcn@latest add https://motionshelf-pi.vercel.app/r/search-ad.json`.
Install
npx shadcn@latest add https://motionshelf-pi.vercel.app/r/search-ad.jsonCopies the file into your project. Requires React 19, Tailwind CSS v4 and GSAP (installed as a dependency).
Props
| Name | Type | Default | Description |
|---|---|---|---|
| query | string | "running shoes" | Text typed into the search box. |
| adDomain | string | "example.com" | Domain shown on the sponsored result. |
| adTitle | string | "Brand · Running shoes on sale" | Title of the sponsored result. |
| adText | string | "Free shipping over $50…" | Description of the sponsored result. |
| organicDomain | string | "reviews.example" | Domain of the organic result. |
| organicTitle | string | "The best running shoes of the year" | Title of the organic result. |
| organicText | string | "We compared the top picks…" | Description of the organic result. |
| cta | string | "Visit store" | Button shown under the results. |
| accent | string | "#1f5bff" | Color of the highlight and the button. |
| loop | boolean | true | Replay forever with a pause between runs. |
| className | string | "" | Extra classes for the outer element. |
Dependencies
- gsap
- @gsap/react