Vanish Input
Search field whose placeholder cycles while empty.
1.4 KB gzippedDeps: motion/reactReduced-motion safeBase UI & Radix compatible
Installation
npx shadcn@latest add https://innsite.com.mx/r/vanish-input.jsonInstalls with: motion
Source
"use client";
import { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
interface VanishInputProps {
/** Cycled through while the field is empty */
placeholders: string[];
/** Seconds each placeholder is held */
interval?: number;
onSubmit?: (value: string) => void;
className?: string;
}
/**
* Search or prompt field whose placeholder cycles while empty. Built on a real
* `form` and `input`, so Enter, autofill and screen readers behave normally.
*/
export function VanishInput({
placeholders,
interval = 3,
onSubmit,
className,
}: VanishInputProps) {
const [index, setIndex] = useState(0);
const [value, setValue] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const reducedMotion = useReducedMotion();
useEffect(() => {
if (placeholders.length < 2 || value) return;
const id = window.setInterval(
() => setIndex((i) => (i + 1) % placeholders.length),
interval * 1000
);
return () => window.clearInterval(id);
}, [placeholders.length, interval, value]);
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
if (!value.trim()) return;
onSubmit?.(value);
setValue("");
inputRef.current?.blur();
};
return (
<form
onSubmit={handleSubmit}
data-slot="vanish-input"
className={cn(
"relative flex h-12 w-full max-w-lg items-center rounded-full border bg-background pr-1.5 pl-5 transition-shadow focus-within:ring-2 focus-within:ring-ring",
className
)}
>
<input
ref={inputRef}
value={value}
onChange={(event) => setValue(event.target.value)}
aria-label={placeholders[0] ?? "Search"}
className="peer h-full flex-1 bg-transparent text-sm outline-none"
/>
{/* Visual only — the input keeps its own accessible label. */}
{!value && (
<span
aria-hidden
className="pointer-events-none absolute left-5 flex h-full items-center overflow-hidden text-sm text-muted-foreground"
>
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={index}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: reducedMotion ? 0 : 0.25 }}
className="block"
>
{placeholders[index]}
</motion.span>
</AnimatePresence>
</span>
)}
<button
type="submit"
aria-label="Submit"
disabled={!value.trim()}
className="grid size-9 shrink-0 place-items-center rounded-full bg-primary text-primary-foreground transition-opacity disabled:opacity-40"
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="M3 8h9M8.5 4l4 4-4 4" />
</svg>
</button>
</form>
);
}

