The Interactive Pets component renders a small bounded playground with a cat, a dog, and a bird. Each pet can be dragged, clicked for a random message, and fed with its matching food.
Beta: Interactive Pets is still in beta. More pets and richer idle animations—such as moving tails, blinking eyes, and other small reactions—are coming soon.
Usage
Installation
Basic Example
import { InteractivePets } from "@/components/interactive-pets";
export default function App() {
return <InteractivePets />;
}
Examples
Cat — Mochi
Mochi the cat, with fish as the feeding action.
Dog — Biscuit
Biscuit the dog, with a bone as the feeding action.
Bird — Pip
Pip the bird, with seeds as the feeding action.
Custom Messages
Override any pet's name, starting position, or messages. Pets you omit are left out entirely.
<InteractivePets
pets={[
{
id: "cat",
name: "Nimbus",
idleMessage: ["hello.", "still watching."],
fedMessage: "an acceptable offering.",
},
{ id: "dog", name: "Waffles", fedMessage: "BEST. DAY. EVER." },
{ id: "bird", name: "Pixel", fedMessage: "chirp of approval" },
]}
/>
Custom Position
initialPosition sets a pet's starting position in pixels from the playground's top-left corner.
<InteractivePets
pets={[
{ id: "cat", initialPosition: { x: 24, y: 40 } },
{ id: "dog", initialPosition: { x: 180, y: 120 } },
]}
/>
Food bowls sit along the bottom by default. Use the data-food-controls styling hook when you need a different position:
<InteractivePets playgroundClassName="[&_[data-food-controls]]:top-6 [&_[data-food-controls]]:bottom-auto" />
Show a Pet Across the Entire Site
Mount InteractivePets inside app/layout.tsx with a fixed, transparent, full-viewport playground. The wrapper ignores pointer events while the pet and bowl remain interactive, so the page underneath still works.
import { InteractivePets } from "@/components/interactive-pets";
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
<body>
{children}
<InteractivePets
pets={[
{
id: "cat",
name: "Mochi",
initialPosition: { x: 24, y: 80 },
idleMessage: ["Hello!", "Welcome back."],
fedMessage: "Thank you!",
},
]}
showInstructions={false}
className="pointer-events-none fixed inset-0 z-40"
playgroundClassName="h-dvh rounded-none border-0 bg-transparent sm:h-dvh"
/>
</body>
</html>
);
}
Because it lives in the root layout, the pet stays visible and draggable across page navigation. Change initialPosition to choose where it starts.
Event Callbacks
onPetMove fires after a drag ends or a keyboard move, with the pet's position in pixels relative to the playground. onPetFeed fires when a food button is pressed.
<InteractivePets
onPetMove={(pet, position) => console.log(`${pet} moved to`, position)}
onPetFeed={(pet) => console.log(`${pet} was fed`)}
/>
Properties
| Prop | Type | Default | Description |
|---|---|---|---|
pets | PetConfig[] | cat, dog, bird | The pets to render. idleMessage accepts one string or an array of randomized click messages. |
className | string | — | Additional classes for the outer wrapper. |
playgroundClassName | string | — | Additional classes for the playground area (e.g. a custom height). |
showInstructions | boolean | true | Whether to show the instruction line below the playground. |
instructionText | string | The office. Drag them anywhere… | Custom instruction text. |
onPetMove | (pet: PetType, position: { x: number; y: number }) => void | — | Called after a pet is dropped or moved with the keyboard. |
onPetFeed | (pet: PetType) => void | — | Called when a pet's food button is pressed. |
Accessibility
- Each pet is focusable and exposes an accessible label. Enter or Space shows a random message.
- Focused pets move with the arrow keys (8px per press, 24px with Shift) and stay inside the playground.
- Food controls are native buttons with descriptive
aria-labels. - Feeding reactions are announced through a polite
aria-liveregion. - A visible focus ring is shown on pets and food buttons.
Reduced Motion
When prefers-reduced-motion is set, idle animations (tail sways, bobbing) are disabled, the food travel animation is skipped, and reaction bubbles appear and disappear without transitions — feeding still shows the reaction bubble and fires callbacks, so nothing is lost besides the motion.