Interactive Pets

A playful playground of draggable office pets — a cat, a dog, and a bird — with feeding reactions and idle animations.

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

$ npx shadcn@latest add https://spark-ui-olive.vercel.app/r/interactive-pets.json

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

PropTypeDefaultDescription
petsPetConfig[]cat, dog, birdThe pets to render. idleMessage accepts one string or an array of randomized click messages.
classNamestringAdditional classes for the outer wrapper.
playgroundClassNamestringAdditional classes for the playground area (e.g. a custom height).
showInstructionsbooleantrueWhether to show the instruction line below the playground.
instructionTextstringThe office. Drag them anywhere…Custom instruction text.
onPetMove(pet: PetType, position: { x: number; y: number }) => voidCalled after a pet is dropped or moved with the keyboard.
onPetFeed(pet: PetType) => voidCalled 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-live region.
  • 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.