A pet on the tab bar
Why would anyone put a panda on a tab bar?
I did it for an app only I use. For a long time it felt like a spreadsheet with a nicer font. Then I gave it a panda. The panda sits on the tab bar, on whichever tab I'm looking at. Tap another tab and it runs over and plops down. That was the whole feature, and it changed how often I opened the app. I wanted to see what the panda was doing.
So I pulled it out into a package. It's called tabpet. It's open source, for React Native or Swift, and the code is at github.com/Lakshay1800/tabpet.
What it does
It sits where you are. It runs when you switch tabs. It chases your finger when you drag along the bar. It climbs off the end of the pill, runs upside down along the bottom, and climbs back up the other side.
Six animals ship with it: panda, cat, turtle, raccoon, bird, squirrel. Each one is three short video clips cut into sprite sheets, plus a profile of springs and speeds. The turtle never hops. The raccoon takes the long way round.
It comes two ways. An Expo app installs react-native-tabpet from npm. It needs Expo SDK 57+, the New Architecture and a dev build, and it is iOS only.
npx expo install react-native-tabpet react-native-reanimated react-native-gesture-handler react-native-worklets expo-image react-native-safe-area-context
npx expo prebuild --clean --platform ios
npx expo run:iosA UIKit or SwiftUI app adds the repo as a Swift package and gets the same six animals. In Xcode, add https://github.com/Lakshay1800/tabpet, then TabPetUIKit and one product per animal, or TabPetAnimals for all six.
The part that's hard
React Native can't draw on the native tab bar. It can draw over it, but it has no idea where the icons are. On iOS 26 that got worse, because the tab bar is now a floating glass pill that's narrower than the screen, and the selection bubble keeps its own copies of the selected button.
I found that out the expensive way. My first measurement matched the wrong button class and returned nothing, so the code fell back to splitting the width evenly. The middle tab looked fine. The end tabs were off by ten points. Then I counted the bubble's copies as buttons, and every seat landed exactly one tab behind: Explore's panda sat on Home, Settings' on Activity. Three rebuilds to learn that a seat that's off by exactly one tab is an index bug, not a spring bug.
The fix is one small Swift module. It walks the UITabBar, skips anything named Lens, and reports each glyph's center and the pill's top edge. In the React Native package that's the only native code in the perch. Everything else is a PNG and a Reanimated worklet. The Swift package does the same measuring and then the rest too, with no Expo, no Reanimated and no JavaScript.
Two mechanisms
The sprite. Each pose is one sprite sheet inside a clipped view. A worklet slides the image one cell per frame on the UI thread, so playback never waits on JavaScript. Run, sit and idle stay mounted and cross-fade, so a pose change never waits on a texture load either. Sit is the resting pose. After the sit-down clip the pet holds its last frame, because sheets cut from different clips almost never share a seam. In the Swift package the animal and its sprite run on one CADisplayLink instead.
The run. A tab change plans a run from the old seat to the new one and drives it with a spring. The finger chase uses a shorter, stiffer spring and a leash, so the pet runs toward your finger and never past it. Every spring completion is checked against a generation counter. Reanimated 4 can report a spring as finished seconds after it visually stopped, and a stale completion must not re-seat a pet that has already left.
The long way round
The raccoon and the squirrel are small and fast, and it bothered me that a tap from the first tab to the last just ran them across the top like everyone else. So they don't. They run off the end of the pill, roll around the corner, run upside down along the underside with their feet on the glass, and climb the far end.
An early version stopped at every corner to turn, and it read like a robot. Another pivoted on the pill's bounding box, so on a rounded pill the feet floated off the glass for most of the wall. The one that shipped is a single path along the pill's real outline, including a half-ellipse around each end cap, run at one constant speed, with the sprite rotating about its own feet. Tap again halfway and it turns around on the glass instead of cutting through it. I verified it from frame stacks of the recording, because no screenshot can tell you whether feet stayed on a curve. The Swift package takes the same route.
What dogfooding found
Before opening it up I put it into two more apps. Each one broke something the tests couldn't see.
The podspec said iOS 16.4 because the app I copied it from said so. The second app targeted 15.1, and CocoaPods skipped the pod silently. The perch rendered, the finger chase didn't, and nothing said why until I grepped the Podfile.lock.
Installing from a local folder made the package a symlink, Metro followed the symlink out of the project, and the app booted a second copy of Reanimated. In Release that crashed with an error about insets that had nothing to do with the cause.
The third app wanted one cat and got all six animals' sheets, about thirteen megabytes of them, because the root import registered everything. Now there's a bare entry and one module per animal, and in Swift one product per animal.
None of those are clever bugs. They're the bugs you only find by being the second user of your own package.
What's next
The code is at github.com/Lakshay1800/tabpet, for React Native and for Swift. The seventh animal is yours: three short clips from Sora, Seedance or Grok Imagine, one shell script, and an afternoon. The art pipeline in the docs has the prompts and the cut recipes.
Every app I've used for years is a tool. The one with the panda in it is the only one that feels like somewhere I go. I want more apps like that. If you put a pet on your tab bar, send me the video.