Search

A live search across experiments and writing. Press ⌘K from anywhere, or click to start typing.


Making of

Working at Warp taught me that great product design isn't about building the most visually impressive thing. It's about making sure that even the most ordinary interactions feel considered.

Search is everywhere on the web, yet most search experiences still feel like an afterthought. Results just snap in and out with no care for how they get there. I wanted to fix that. The small stuff: a ⌘K shortcut (a habit from Warp I reach for everywhere), a focus ring that feels intentional. But the real work was in the result list itself.

The hardest part was making sure the list never feels mechanical as you type. Every keystroke potentially adds or removes results, and the remaining items have to reflow into new positions. Getting that to feel smooth in both directions took real iteration.

When filtering gets more specific and a result leaves, the exiting item has to disappear cleanly without flickering over the results that are sliding into its place. The fix was wrapping each result in overflow-hidden. Without it, the fading-out element renders on top of the items repositioning underneath it, creating a visible overlap. Clipping the exit to its own bounds keeps everything clean.

// overflow-hidden clips the exit animation to the item's own bounds,
// preventing it from rendering over results repositioning below it
<motion.div key={item.id} layout {...resultMotion} className="overflow-hidden">
  {/* search result content */}
</motion.div>

The opposite problem happens when filtering gets less specific and a result comes back. Without a delay, the incoming item fades in while the list is still mid-reflow, landing on top of content that hasn't finished moving yet. Adding a short delay to the enter animation gives the layout shift time to settle first, so the new result only appears once there's a clear space for it.

const resultMotion = {
  initial: { opacity: 0, scale: 0.97 },
  animate: {
    opacity: 1,
    scale: 1,
    // delay lets the layout shift settle before the item fades in
    transition: { delay: 0.18, duration: 0.12, ease: 'easeOut' },
  },
  exit: { opacity: 0, transition: { duration: 0.1 } },
  // spring physics for the position reflow
  transition: { layout: { type: 'spring', stiffness: 200, damping: 30 } },
}

Putting it together: each result gets the layout prop so Framer Motion animates its position whenever the list changes. AnimatePresence with initial={false} handles enter and exit without animating results that were already visible on mount.

<AnimatePresence initial={false}>
  {results.map((item) => (
    <motion.div key={item.id} layout {...resultMotion} className="overflow-hidden">
      {/* search result content */}
    </motion.div>
  ))}
</AnimatePresence>