{"slug":"how-i-built-the-floating-pill-header","locale":"en","isFallback":false,"translationAvailable":["en","id"],"translationUrls":{"en":"/api/notes/how-i-built-the-floating-pill-header?locale=en","id":"/api/notes/how-i-built-the-floating-pill-header?locale=id"},"title":"Building a Floating Pill Header with Next.js and Tailwind","description":"A technical deep dive into building a responsive, multi-view floating header using React Hooks and Tailwind CSS.","date":"2026-02-17","updated":"2026-02-27","tags":["nextjs","react","tailwindcss","ui-design"],"content":"\n![Building a Floating Pill Header: Feature Breakdown with Next.js and Tailwind](/images/_notes/phill-header/phill-header.webp)\n\nIf you've navigated this site, you've likely interacted with the floating \"pill\" header at the top of the page. It's designed to be minimal yet powerful, housing navigation, search, and a reading list feature all in one compact, responsive component. In this note, I'll break down how it was built using Next.js, React Hooks, and Tailwind CSS.\n\n### Core Technologies\n\n- **Next.js App Router:** For the component structure and server-side rendering.\n- **React Hooks:** Primarily `useState`, `useEffect`, and `useRef` to manage state, side effects, and direct DOM references.\n- **Tailwind CSS:** For all styling, enabling rapid, utility-first design. We also use `clsx` for conditionally applying classes.\n- **Lucide React:** For the icons.\n\n### Step 1: The Basic Structure & Floating Position\n\nThe foundation is a `<header>` element with `position: fixed`.\n\n```jsx\n<header className=\"fixed top-4 left-4 right-4 md:left-1/2 md:-translate-x-1/2 ... z-50\">\n  <nav className=\"relative ... rounded-full bg-primary/90 backdrop-blur-sm ...\">\n    {/* Content goes here */}\n  </nav>\n</header>\n```\n- `fixed top-4`: This pins the header near the top of the viewport.\n- `left-4 right-4`: On mobile, it spans most of the width.\n- `md:left-1/2 md:-translate-x-1/2`: On medium screens and up, it centers itself horizontally.\n- `bg-primary/90 backdrop-blur-sm`: This creates the semi-transparent, blurred glass effect.\n- `rounded-full`: This gives it the \"pill\" shape.\n\n### Step 2: The Brain - View State Management\n\nThe most critical part of the header is managing what is currently being displayed. I use a single `useState` hook for this:\n\n```tsx\ntype ActiveView = 'none' | 'search' | 'menu' | 'readingList';\nconst [activeView, setActiveView] = useState<ActiveView>('none');\n```\n\nEvery interactive element in the header simply calls `setActiveView` to change what is shown. For example, clicking the search icon sets the view to `'search'`, which in turn triggers conditional rendering for the search input and results panel.\n\nThe component's shape and styles also react to this state. When `isSearchOpen` is true, the `nav` element's width expands. When `isMenuOpen` is true, the `border-radius` changes from `rounded-full` to `rounded-t-2xl` to connect with the dropdown panel below it.\n\n### Step 3: Dynamic Search Experience\n\nThe search functionality is a key feature.\n1.  **State Transition:** Clicking the search icon sets `activeView = 'search'`.\n2.  **Conditional Animation:** CSS classes are toggled using `clsx`. The main navigation links fade out (`opacity-0`) while the search input container fades in (`opacity-100`).\n3.  **Auto-focus:** A `useEffect` hook triggers when the search view becomes active, automatically focusing the `<input>` element for a seamless user experience.\n    ```tsx\n    const searchInputRef = useRef<HTMLInputElement>(null);\n\n    useEffect(() => {\n      if (isSearchOpen) {\n        // A short timeout ensures the element is visible before focusing\n        setTimeout(() => searchInputRef.current?.focus(), 100);\n      }\n    }, [isSearchOpen]);\n    ```\n4.  **Real-time Filtering:** Another `useEffect` watches for changes in the search `query` state. It filters a pre-loaded array of all posts and notes to show relevant results instantly.\n\n### Step 4: Smart Visibility on Scroll\n\nA common UX pattern is to hide the navigation on scroll-down and show it on scroll-up. This is achieved with another `useEffect` hook.\n\n```tsx\nconst lastScrollY = useRef(0);\nconst [isVisible, setIsVisible] = useState(true);\n\nuseEffect(() => {\n  const handleScroll = () => {\n    const currentScrollY = window.scrollY;\n    // Show if scrolling up or near the top\n    if (currentScrollY < lastScrollY.current || currentScrollY < 10) {\n      setIsVisible(true);\n    } else if (currentScrollY > 100 && currentScrollY > lastScrollY.current) {\n      // Hide if scrolling down and past a certain threshold\n      setIsVisible(false);\n    }\n    lastScrollY.current = currentScrollY;\n  };\n\n  window.addEventListener('scroll', handleScroll, { passive: true });\n  return () => window.removeEventListener('scroll', handleScroll);\n}, []);\n```\nThe `isVisible` state then adds or removes classes that control the header's `opacity` and `transform`.\n\n### Step 5: The Little Details\n\n- **Escape Key:** A global event listener is set up to close any active view (search, menu, etc.) when the 'Escape' key is pressed.\n- **Click Outside:** A similar listener closes the views if the user clicks anywhere outside the header component.\n- **Responsiveness:** The component gracefully transitions from a full-width mobile header to a compact, centered desktop header using Tailwind's responsive prefixes (`md:`).\n\n---\n\n### Precision Tech Update (February 27, 2026)\n\nAs part of the SnipGeek **\"Precision Tech\"** design overhaul, I have transitioned most of the site's components—such as article cards, code blocks, and images—to a sharper **4px corner radius**. \n\nHowever, I intentionally decided to keep the header in its original **full-rounded \"Pill\" shape**. This creates a deliberate visual contrast, ensuring the navigation remains a distinct, floating element that feels light and fluid against the more structured, technical layout of the content below. This serves as a permanent record of our design evolution.\n"}