<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged tanstack</title>
<link>https://coderlegion.com/tag/tanstack</link>
<description>Powered by Question2Answer</description>
<item>
<title>Build a Slide-In Mobile Menu in TanStack Start with Tailwind v4</title>
<link>https://coderlegion.com/31644/build-a-slide-in-mobile-menu-in-tanstack-start-with-tailwind-v4</link>
<description>&lt;p&gt;A mobile menu that pops in and out works, but it feels unfinished. In this article we turn it into a drawer that slides in from the right with a fading backdrop, using only Tailwind classes and no animation library. We also keep it usable for keyboard and screen-reader users, lock the page scroll while it is open, and respect the reduced-motion setting.&lt;/p&gt;
&lt;p&gt;This is part of the &lt;strong&gt;TanStack Start Basics&lt;/strong&gt; series. It builds on the responsive &lt;code&gt;Header&lt;/code&gt; and the separate &lt;code&gt;NavLinks&lt;/code&gt; component from the previous part.&lt;/p&gt;
&lt;p&gt;You can view the video &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=4oE4P7uGF3Y&quot;&gt;Here&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;remove-the-old-mobile-menu&quot;&gt;Remove the old mobile menu&lt;/h3&gt;
&lt;p&gt;In the previous part, the mobile menu lived in the &lt;code&gt;Header&lt;/code&gt;, underneath the flex container, and the hamburger button toggled it. Delete it. This is the code to remove:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;{open &amp;amp;&amp;amp; (
  &amp;lt;nav
    id=&quot;mobile-menu&quot;
    aria-label=&quot;Mobile&quot;
    className=&quot;border-t md:hidden&quot;
  &amp;gt;
    &amp;lt;NavLinks
      listClass=&quot;flex flex-col px-6 py-2&quot;
      linkClass=&quot;block py-3 text-base font-medium text-neutral-600 hover:text-black&quot;
      onNavigate={() =&amp;gt; setOpen(false)}
    /&amp;gt;
  &amp;lt;/nav&amp;gt;
)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When &lt;code&gt;open&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;, React removes this menu from the page completely. A CSS transition needs an element that exists in both the start and end states, so there is nothing to animate. The fix is a side panel that is always on the page, which we slide off screen with Tailwind.&lt;/p&gt;
&lt;h3 id=&quot;always-render-the-panel&quot;&gt;Always render the panel&lt;/h3&gt;
&lt;p&gt;Here is the side panel. It replaces the old menu in the same position in the header. It uses a &lt;code&gt;cn&lt;/code&gt; helper, so you will also need this function in the &lt;code&gt;utils&lt;/code&gt; file in your &lt;code&gt;lib&lt;/code&gt; folder. If you use shadcn/ui, you already have it. Otherwise, install &lt;code&gt;clsx&lt;/code&gt; and &lt;code&gt;tailwind-merge&lt;/code&gt; (&lt;code&gt;npm i clsx tailwind-merge&lt;/code&gt;) and add:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/lib/utils.ts
import type { ClassValue } from 'clsx'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The panel also uses the &lt;code&gt;X&lt;/code&gt; icon, so add these imports at the top of &lt;code&gt;Header.tsx&lt;/code&gt; if you don't have them yet:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;nav
  id=&quot;mobile-menu&quot;
  aria-label=&quot;Mobile&quot;
  className={cn(
    'fixed inset-y-0 right-0 z-50 w-72 bg-white p-6 shadow-xl md:hidden',
  )}
&amp;gt;
  &amp;lt;button
    type=&quot;button&quot;
    onClick={() =&amp;gt; setOpen(false)}
    aria-label=&quot;Close menu&quot;
    className=&quot;mb-6 ml-auto block rounded p-2 hover:bg-gray-100&quot;
  &amp;gt;
    &amp;lt;X className=&quot;size-6&quot; /&amp;gt;
  &amp;lt;/button&amp;gt;
  &amp;lt;NavLinks
    listClass=&quot;flex flex-col gap-4&quot;
    linkClass=&quot;text-lg text-gray-600 hover:text-black&quot;
    onNavigate={() =&amp;gt; setOpen(false)}
  /&amp;gt;
&amp;lt;/nav&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;make-it-jump-in-and-out&quot;&gt;Make it jump in and out&lt;/h3&gt;
&lt;p&gt;To move the panel on and off the screen, add Tailwind's translate classes with a ternary on &lt;code&gt;open&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;open ? 'translate-x-0' : 'translate-x-full',
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add it as a second argument to &lt;code&gt;cn&lt;/code&gt; on the &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;nav
  id=&quot;mobile-menu&quot;
  aria-label=&quot;Mobile&quot;
  className={cn(
    'fixed inset-y-0 right-0 z-50 w-72 bg-white p-6 shadow-xl md:hidden',
    open ? 'translate-x-0' : 'translate-x-full',
  )}
&amp;gt;
  {/* the close button and NavLinks stay the same */}
&amp;lt;/nav&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The panel now jumps on and off the screen, but it doesn't slide yet.&lt;/p&gt;
&lt;p&gt;You may have noticed the helper function &lt;code&gt;cn&lt;/code&gt;. It joins class names, skips the ones that are false, and if two Tailwind classes conflict, the last one wins.&lt;/p&gt;
&lt;h3 id=&quot;add-the-backdrop&quot;&gt;Add the backdrop&lt;/h3&gt;
&lt;p&gt;Next, a dark layer behind the panel. Clicking it closes the menu, which is what people expect from a drawer. It sits one layer below the panel (&lt;code&gt;z-40&lt;/code&gt; against &lt;code&gt;z-50&lt;/code&gt;) and covers the whole screen. When the menu is closed it must not catch clicks, so &lt;code&gt;pointer-events-none&lt;/code&gt; is not optional here. Without it, an invisible full-screen layer would swallow every tap on the page. Put it right above the &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;div
  aria-hidden=&quot;true&quot;
  onClick={() =&amp;gt; setOpen(false)}
  className={cn(
    'fixed inset-0 z-40 bg-black/50 md:hidden',
    open ? 'opacity-100' : 'pointer-events-none opacity-0',
  )}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is how it should look in your code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;div
  aria-hidden=&quot;true&quot;
  onClick={() =&amp;gt; setOpen(false)}
  className={cn(
    'fixed inset-0 z-40 bg-black/50 md:hidden',
    open ? 'opacity-100' : 'pointer-events-none opacity-0',
  )}
/&amp;gt;
&amp;lt;nav
  id=&quot;mobile-menu&quot;
  aria-label=&quot;Mobile&quot;
  className={cn(
    'fixed inset-y-0 right-0 z-50 w-72 bg-white p-6 shadow-xl md:hidden',
    open ? 'translate-x-0' : 'translate-x-full',
  )}
&amp;gt;
  &amp;lt;button
    type=&quot;button&quot;
    onClick={() =&amp;gt; setOpen(false)}
    aria-label=&quot;Close menu&quot;
    className=&quot;mb-6 ml-auto block rounded p-2 hover:bg-gray-100&quot;
  &amp;gt;
    &amp;lt;X className=&quot;size-6&quot; /&amp;gt;
  &amp;lt;/button&amp;gt;
  &amp;lt;NavLinks
    listClass=&quot;flex flex-col gap-4&quot;
    linkClass=&quot;text-lg text-gray-600 hover:text-black&quot;
    onNavigate={() =&amp;gt; setOpen(false)}
  /&amp;gt;
&amp;lt;/nav&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;make-it-slide&quot;&gt;Make it slide&lt;/h3&gt;
&lt;p&gt;Now the fun part, and it takes two classes. &lt;code&gt;transition-transform&lt;/code&gt; tells the browser to animate changes to the position, &lt;code&gt;duration-300&lt;/code&gt; makes it take 300 milliseconds, and &lt;code&gt;ease-out&lt;/code&gt; makes it start fast and settle gently. The backdrop gets the same treatment, but it fades, so it uses &lt;code&gt;transition-opacity&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Add this to the panel:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;transition-transform duration-300 ease-out
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add this to the backdrop:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;transition-opacity duration-300
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your code should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;div
  aria-hidden=&quot;true&quot;
  onClick={() =&amp;gt; setOpen(false)}
  className={cn(
    'fixed inset-0 z-40 bg-black/50 transition-opacity duration-300 md:hidden',
    open ? 'opacity-100' : 'pointer-events-none opacity-0',
  )}
/&amp;gt;
&amp;lt;nav
  id=&quot;mobile-menu&quot;
  aria-label=&quot;Mobile&quot;
  className={cn(
    'fixed inset-y-0 right-0 z-50 w-72 bg-white p-6 shadow-xl transition-transform duration-300 ease-out md:hidden',
    open ? 'translate-x-0' : 'translate-x-full',
  )}
&amp;gt;
  &amp;lt;button
    type=&quot;button&quot;
    onClick={() =&amp;gt; setOpen(false)}
    aria-label=&quot;Close menu&quot;
    className=&quot;mb-6 ml-auto block rounded p-2 hover:bg-gray-100&quot;
  &amp;gt;
    &amp;lt;X className=&quot;size-6&quot; /&amp;gt;
  &amp;lt;/button&amp;gt;
  &amp;lt;NavLinks
    listClass=&quot;flex flex-col gap-4&quot;
    linkClass=&quot;text-lg text-gray-600 hover:text-black&quot;
    onNavigate={() =&amp;gt; setOpen(false)}
  /&amp;gt;
&amp;lt;/nav&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One Tailwind v4 detail: &lt;code&gt;translate-x-*&lt;/code&gt; sets the CSS &lt;code&gt;translate&lt;/code&gt; property, not &lt;code&gt;transform&lt;/code&gt;. In v4, &lt;code&gt;transition-transform&lt;/code&gt; includes &lt;code&gt;translate&lt;/code&gt;, so this works. If a slide ever refuses to animate, check that your transition covers &lt;code&gt;translate&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;hide-the-closed-panel&quot;&gt;Hide the closed panel&lt;/h3&gt;
&lt;p&gt;It looks right, but there is a catch. The panel is only parked off screen. Press Tab with the menu closed and focus disappears into links you can't see, and a screen reader will still read them out. The fix is the &lt;code&gt;invisible&lt;/code&gt; class when the menu is closed.&lt;/p&gt;
&lt;p&gt;If we add &lt;code&gt;invisible&lt;/code&gt; and nothing else, the panel vanishes the instant you close it, and the slide-out never shows. So we also tell the transition to animate &lt;code&gt;visibility&lt;/code&gt;, not just position. The browser keeps the panel visible until the slide finishes, then hides it.&lt;/p&gt;
&lt;p&gt;Make two changes to the panel:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Replace &lt;code&gt;transition-transform&lt;/code&gt; with &lt;code&gt;transition-[translate,visibility]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;translate-x-full&lt;/code&gt; with &lt;code&gt;invisible translate-x-full&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Your code should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;div
  aria-hidden=&quot;true&quot;
  onClick={() =&amp;gt; setOpen(false)}
  className={cn(
    'fixed inset-0 z-40 bg-black/50 transition-opacity duration-300 md:hidden',
    open ? 'opacity-100' : 'pointer-events-none opacity-0',
  )}
/&amp;gt;
&amp;lt;nav
  id=&quot;mobile-menu&quot;
  aria-label=&quot;Mobile&quot;
  className={cn(
    'fixed inset-y-0 right-0 z-50 w-72 bg-white p-6 shadow-xl transition-[translate,visibility] duration-300 ease-out md:hidden',
    open ? 'translate-x-0' : 'invisible translate-x-full',
  )}
&amp;gt;
  &amp;lt;button
    type=&quot;button&quot;
    onClick={() =&amp;gt; setOpen(false)}
    aria-label=&quot;Close menu&quot;
    className=&quot;mb-6 ml-auto block rounded p-2 hover:bg-gray-100&quot;
  &amp;gt;
    &amp;lt;X className=&quot;size-6&quot; /&amp;gt;
  &amp;lt;/button&amp;gt;
  &amp;lt;NavLinks
    listClass=&quot;flex flex-col gap-4&quot;
    linkClass=&quot;text-lg text-gray-600 hover:text-black&quot;
    onNavigate={() =&amp;gt; setOpen(false)}
  /&amp;gt;
&amp;lt;/nav&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;stop-the-page-from-scrolling&quot;&gt;Stop the page from scrolling&lt;/h3&gt;
&lt;p&gt;With the drawer open, scrolling still moves the page behind it, which feels sloppy. We already have an effect that listens for the Escape key, so we extend it: lock scrolling when the menu opens, and unlock it in the cleanup. Replace the &lt;code&gt;useEffect&lt;/code&gt; from the previous part, the one that closes the menu on Escape, with this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;useEffect(() =&amp;gt; {
  if (!open) return

  const onKeyDown = (e: KeyboardEvent) =&amp;gt; {
    if (e.key === 'Escape') setOpen(false)
  }

  document.addEventListener('keydown', onKeyDown)
  document.body.style.overflow = 'hidden'

  return () =&amp;gt; {
    document.removeEventListener('keydown', onKeyDown)
    document.body.style.overflow = ''
  }
}, [open])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The cleanup function runs when the menu closes and also if the &lt;code&gt;Header&lt;/code&gt; ever unmounts, so the page can never get stuck locked.&lt;/p&gt;
&lt;h3 id=&quot;respect-reduced-motion-optional-&quot;&gt;Respect reduced motion (optional)&lt;/h3&gt;
&lt;p&gt;Some people turn on &quot;reduce motion&quot; in their system settings because animation makes them dizzy. Tailwind gives us a variant for that, &lt;code&gt;motion-reduce:&lt;/code&gt;, so honoring it costs one class per element. If that's you, add &lt;code&gt;motion-reduce:transition-none&lt;/code&gt; to the panel and the backdrop, like so.&lt;/p&gt;
&lt;p&gt;Panel:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;className={cn(
  'fixed inset-y-0 right-0 z-50 w-72 bg-white p-6 shadow-xl transition-[translate,visibility] duration-300 ease-out motion-reduce:transition-none md:hidden',
  open ? 'translate-x-0' : 'invisible translate-x-full',
)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Backdrop:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;className={cn(
  'fixed inset-0 z-40 bg-black/50 transition-opacity duration-300 motion-reduce:transition-none md:hidden',
  open ? 'opacity-100' : 'pointer-events-none opacity-0',
)}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;test-it-and-wrap-up&quot;&gt;Test it and wrap up&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Open the menu: the panel slides in and the backdrop fades.&lt;/li&gt;
&lt;li&gt;Click the backdrop: the menu closes.&lt;/li&gt;
&lt;li&gt;Open it again and press Escape: it closes.&lt;/li&gt;
&lt;li&gt;Open it and click a link: it navigates and closes.&lt;/li&gt;
&lt;li&gt;Close the menu and press Tab: focus skips the hidden links.&lt;/li&gt;
&lt;li&gt;Open the menu and try to scroll the page: it stays put.&lt;/li&gt;
&lt;li&gt;Turn on reduced motion in your system settings: the menu appears with no animation.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let's recap. We always render the panel and move it with &lt;code&gt;translate&lt;/code&gt;. The backdrop fades and closes the menu. &lt;code&gt;transition-[translate,visibility]&lt;/code&gt; keeps the slide-out visible, then hides the panel from keyboards and screen readers. We locked the page scroll, and we respect reduced motion. All of it is plain Tailwind classes and one small effect.&lt;/p&gt;
&lt;h3 id=&quot;good-to-know-and-what-to-try-next&quot;&gt;Good to know and what to try next&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;A gotcha:&lt;/strong&gt; a &lt;code&gt;fixed&lt;/code&gt; element is positioned against the viewport unless an ancestor has a &lt;code&gt;transform&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt; or &lt;code&gt;backdrop-filter&lt;/code&gt;. If your header uses &lt;code&gt;backdrop-blur&lt;/code&gt;, move the backdrop and panel outside the &lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt; element or the drawer will be sized to the header instead of the screen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tell screen readers the menu state:&lt;/strong&gt; the hamburger button from the previous part should announce whether the menu is open. Add &lt;code&gt;aria-expanded&lt;/code&gt; and &lt;code&gt;aria-controls&lt;/code&gt; (the &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; already has &lt;code&gt;id=&quot;mobile-menu&quot;&lt;/code&gt;), and keep the label constant, because the state comes from &lt;code&gt;aria-expanded&lt;/code&gt; and a label that flips between &quot;Open&quot; and &quot;Close&quot; can make it get announced twice. If your button already has a label like &quot;Open menu&quot;, change it to a fixed one:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;button
  type=&quot;button&quot;
  onClick={() =&amp;gt; setOpen((o) =&amp;gt; !o)}
  aria-expanded={open}
  aria-controls=&quot;mobile-menu&quot;
  aria-label=&quot;Menu&quot;
&amp;gt;
  {/* your hamburger icon */}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Try next:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Close the menu automatically when the window grows past the &lt;code&gt;md&lt;/code&gt; breakpoint, so the scroll lock can never be left on.&lt;/li&gt;
&lt;li&gt;Move focus to the Close button when the menu opens, and back to the hamburger when it closes.&lt;/li&gt;
&lt;li&gt;Swap the slide for a left-side drawer by changing &lt;code&gt;right-0&lt;/code&gt; to &lt;code&gt;left-0&lt;/code&gt; and &lt;code&gt;translate-x-full&lt;/code&gt; to &lt;code&gt;-translate-x-full&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next in the series: error handling and a proper not-found page.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31644/build-a-slide-in-mobile-menu-in-tanstack-start-with-tailwind-v4</guid>
<pubDate>Sun, 18 Oct 2026 18:49:44 +0000</pubDate>
</item>
<item>
<title>Responsive Navigation in TanStack Start: Hamburger Menu and Desktop Navbar</title>
<link>https://coderlegion.com/31251/responsive-navigation-in-tanstack-start-hamburger-menu-and-desktop-navbar</link>
<description>&lt;p&gt;A header that works on a phone and on a desktop is one of the first things every site needs. In this part of the TanStack Start Basics series, we'll build one with inline links on wide screens, a hamburger menu on small ones, and a few accessibility details that are easy to skip.&lt;/p&gt;
&lt;p&gt;You can watch the video here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=_-0DDuQseSo&quot;&gt;Responsive Navigation&lt;/a&gt;. Everything below is the written version.&lt;/p&gt;
&lt;h4 id=&quot;write-the-links-once&quot;&gt;Write the links once&lt;/h4&gt;
&lt;p&gt;Start with a single list of links that both menus will read from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/lib/nav.ts
export const navLinks = [
  { to: '/', label: 'Home' },
  { to: '/products', label: 'Products' },
  { to: '/about', label: 'About' },
  { to: '/contact', label: 'Contact' },
] as const;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;as const&lt;/code&gt; lets TypeScript treat every &lt;code&gt;to&lt;/code&gt; as an exact route, so a typo is a compile error and not a broken link.&lt;/p&gt;
&lt;h4 id=&quot;think-mobile-first&quot;&gt;Think mobile first&lt;/h4&gt;
&lt;p&gt;Tailwind is mobile first. Classes without a prefix apply to every screen size, and a prefix such as &lt;code&gt;md:&lt;/code&gt; applies from that width upward. So the plan is to style the phone version first, and then add the desktop changes on top:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hidden md:block&lt;/code&gt; is hidden on phones and shown from 768px up (the desktop links)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;md:hidden&lt;/code&gt; is shown on phones and hidden from 768px up (the menu button and the mobile menu)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;a-separate-navlinks-component&quot;&gt;A separate NavLinks component&lt;/h4&gt;
&lt;p&gt;Both menus need the same list of links, so it gets its own component. Start with the props type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/types/index.ts
export type NavLinksProps = {
  listClass: string;
  linkClass: string;
  activeClass?: string;
  onNavigate?: () =&amp;gt; void;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the component:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// src/components/NavLinks.tsx
import { Link } from '@tanstack/react-router';
import { navLinks } from '@/lib/nav';
import type { NavLinksProps } from '@/types';

export default function NavLinks({
  listClass,
  linkClass,
  activeClass = 'text-black underline underline-offset-4',
  onNavigate,
}: NavLinksProps) {
  return (
    &amp;lt;ul className={listClass}&amp;gt;
      {navLinks.map((link) =&amp;gt; (
        &amp;lt;li key={link.to}&amp;gt;
          &amp;lt;Link
            to={link.to}
            activeOptions={{ exact: link.to === '/' }}
            className={linkClass}
            activeProps={{ className: activeClass }}
            onClick={onNavigate}
          &amp;gt;
            {link.label}
          &amp;lt;/Link&amp;gt;
        &amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The two class props let each menu style the list its own way. &lt;code&gt;activeClass&lt;/code&gt; has a default, so most callers can leave it out, and &lt;code&gt;onNavigate&lt;/code&gt; is optional, so the desktop menu doesn't need it. Because the component lives in its own file, you can reuse it later, in a footer for example, without copying anything.&lt;/p&gt;
&lt;h4 id=&quot;the-header&quot;&gt;The header&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// src/components/Header.tsx
import { useEffect, useState } from 'react';
import { Link } from '@tanstack/react-router';
import { Menu, X } from 'lucide-react';
import NavLinks from '@/components/NavLinks';

export default function Header() {
  const [open, setOpen] = useState(false);

  useEffect(() =&amp;gt; {
    if (!open) return;
    const onKeyDown = (e: KeyboardEvent) =&amp;gt; {
      if (e.key === 'Escape') setOpen(false);
    };
    window.addEventListener('keydown', onKeyDown);
    return () =&amp;gt; window.removeEventListener('keydown', onKeyDown);
  }, [open]);

  return (
    &amp;lt;header className=&quot;border-b bg-white&quot;&amp;gt;
      &amp;lt;div className=&quot;mx-auto flex h-16 max-w-6xl items-center justify-between px-6&quot;&amp;gt;
        &amp;lt;Link to=&quot;/&quot; className=&quot;text-lg font-bold&quot;&amp;gt;
          My Store
        &amp;lt;/Link&amp;gt;

        &amp;lt;nav aria-label=&quot;Main&quot; className=&quot;hidden md:block&quot;&amp;gt;
          &amp;lt;NavLinks
            listClass=&quot;flex items-center gap-6&quot;
            linkClass=&quot;text-sm font-medium text-neutral-600 hover:text-black&quot;
          /&amp;gt;
        &amp;lt;/nav&amp;gt;

        &amp;lt;button
          type=&quot;button&quot;
          onClick={() =&amp;gt; setOpen((o) =&amp;gt; !o)}
          aria-label={open ? 'Close menu' : 'Open menu'}
          aria-expanded={open}
          aria-controls=&quot;mobile-menu&quot;
          className=&quot;flex size-11 items-center justify-center rounded-md md:hidden&quot;
        &amp;gt;
          {open ? &amp;lt;X className=&quot;size-6&quot; /&amp;gt; : &amp;lt;Menu className=&quot;size-6&quot; /&amp;gt;}
        &amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;

      {open &amp;amp;&amp;amp; (
        &amp;lt;nav
          id=&quot;mobile-menu&quot;
          aria-label=&quot;Mobile&quot;
          className=&quot;border-t md:hidden&quot;
        &amp;gt;
          &amp;lt;NavLinks
            listClass=&quot;flex flex-col px-6 py-2&quot;
            linkClass=&quot;block py-3 text-base font-medium text-neutral-600 hover:text-black&quot;
            onNavigate={() =&amp;gt; setOpen(false)}
          /&amp;gt;
        &amp;lt;/nav&amp;gt;
      )}
    &amp;lt;/header&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;closing-the-menu-when-you-navigate&quot;&gt;Closing the menu when you navigate&lt;/h4&gt;
&lt;p&gt;If you skip this, you'll notice a bug: you tap a link, the page changes, and the menu stays open. The header lives in a layout route, and a layout stays mounted while the pages inside it change, so its state is never reset.&lt;/p&gt;
&lt;p&gt;The fix is to close the menu in the link's click handler, which is where the user's action happens. React's documentation recommends handling things caused by a user action in the event handler for that action, and not in an effect. So &lt;code&gt;NavLinks&lt;/code&gt; gets an optional &lt;code&gt;onNavigate&lt;/code&gt; prop, and only the mobile menu passes &lt;code&gt;() =&amp;gt; setOpen(false)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This also closes the menu when you tap the link for the page you're already on, which an effect that watches the path would miss. If you also want the menu to close when the user presses the browser's back button, you can add an effect that watches the path on top of this, but the click handler covers the main case.&lt;/p&gt;
&lt;h4 id=&quot;active-links&quot;&gt;Active links&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Link&lt;/code&gt; knows when it points at the current page, and &lt;code&gt;activeProps&lt;/code&gt; lets you style that state. Every path starts with a slash, so the home link would look active everywhere. &lt;code&gt;activeOptions={{ exact: true }}&lt;/code&gt; fixes that, and the code above applies it only to the home link.&lt;/p&gt;
&lt;h4 id=&quot;accessibility-basics&quot;&gt;Accessibility basics&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Use a real &lt;code&gt;button&lt;/code&gt; for the toggle, with &lt;code&gt;type=&quot;button&quot;&lt;/code&gt;, so the keyboard can reach it&lt;/li&gt;
&lt;li&gt;Label it with &lt;code&gt;aria-label&lt;/code&gt;, and update the label with the state&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;aria-expanded&lt;/code&gt; and &lt;code&gt;aria-controls&lt;/code&gt; so assistive technology knows whether the menu is open&lt;/li&gt;
&lt;li&gt;Close the menu with the Escape key&lt;/li&gt;
&lt;li&gt;Give each &lt;code&gt;nav&lt;/code&gt; its own label, so a screen reader user can tell them apart&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;use-it-in-the-layout&quot;&gt;Use it in the layout&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// src/routes/_appLayout.tsx
import { createFileRoute, Outlet } from '@tanstack/react-router';
import Header from '@/components/Header';

export const Route = createFileRoute('/_appLayout')({
  component: AppLayout,
});

function AppLayout() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Header /&amp;gt;
      &amp;lt;main className=&quot;mx-auto max-w-6xl px-6 py-8&quot;&amp;gt;
        &amp;lt;Outlet /&amp;gt;
      &amp;lt;/main&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;try-it&quot;&gt;Try it&lt;/h4&gt;
&lt;p&gt;Open your browser's device toolbar and drag the width between 375px and 1024px. The hamburger should appear below 768px, and the inline links above it. Tap a link in the mobile menu and check that it closes.&lt;/p&gt;
&lt;h4 id=&quot;what-s-next&quot;&gt;What's next&lt;/h4&gt;
&lt;p&gt;In the next part, we'll look at slide-in animation and a dark mode toggle. If you got stuck anywhere, leave a comment on the video.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31251/responsive-navigation-in-tanstack-start-hamburger-menu-and-desktop-navbar</guid>
<pubDate>Fri, 16 Oct 2026 02:14:20 +0000</pubDate>
</item>
<item>
<title>TanStack Start Layouts Explained (Pathless Layout Routes + Outlet)</title>
<link>https://coderlegion.com/28879/tanstack-start-layouts-explained-pathless-layout-routes-outlet</link>
<description>&lt;p&gt;Layouts let us customize sections of our app based on their purpose, their content, or the look and feel we want. They give us the flexibility to mix things up, get more creative, and offer a really great UI/UX experience to our users.&lt;/p&gt;
&lt;p&gt;Navigation benefits the most. With layouts we can change the nav style, content, and links based on the page a user is on. We also get to write &lt;strong&gt;DRY&lt;/strong&gt; (Don't Repeat Yourself) code: build a navigation component once, call it in a layout, and when something needs to change, edit one file and the change propagates throughout the app.&lt;/p&gt;
&lt;p&gt;So, enough talk. Let's crack on.&lt;/p&gt;
&lt;h3 id=&quot;where-we-re-picking-up&quot;&gt;Where we're picking up&lt;/h3&gt;
&lt;p&gt;This post continues the app we've been building in this series. If you're here for the first time, start with the first post in this &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=dEqDPVPYRDI&amp;amp;list=PLVNnZvjyvjgo&quot;&gt;TanStack Start series&lt;/a&gt;. If you want to keep up with the full stack, you can also go through the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=tJGuxqhv2SY&amp;amp;list=PLCsGGtToskOE&quot;&gt;Directus series&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;create-a-simple-header&quot;&gt;Create a simple header&lt;/h3&gt;
&lt;p&gt;Open the app in VS Code. In &lt;code&gt;src/components&lt;/code&gt;, create a file called &lt;code&gt;MainHeader.tsx&lt;/code&gt; with the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export default function MainHeader() {
  return &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the div, add another div containing an &lt;code&gt;h1&lt;/code&gt; and a &lt;code&gt;p&lt;/code&gt; tag:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export default function MainHeader() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;div&amp;gt;
        &amp;lt;h1&amp;gt;Logo&amp;lt;/h1&amp;gt;
        &amp;lt;p&amp;gt;This is our navigtion menu&amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yes, &quot;navigtion&quot; is missing an &quot;a&quot;. That's deliberate, and you'll see why shortly.&lt;/p&gt;
&lt;p&gt;Now add some Tailwind CSS classes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export default function MainHeader() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;div className=&quot;flex items-center justify-between p-8&quot;&amp;gt;
        &amp;lt;h1 className=&quot;uppercase font-bold text-2xl&quot;&amp;gt;Logo&amp;lt;/h1&amp;gt;
        &amp;lt;p&amp;gt;This is our navigtion menu&amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;approach-1-paste-the-header-into-every-route&quot;&gt;Approach 1: paste the header into every route&lt;/h3&gt;
&lt;p&gt;Copy the div and its content from &lt;code&gt;MainHeader&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;div&amp;gt;
  &amp;lt;div className=&quot;flex items-center justify-between p-8&quot;&amp;gt;
    &amp;lt;h1 className=&quot;uppercase font-bold text-2xl&quot;&amp;gt;Logo&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;This is our navigtion menu&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Paste it into each route component (index, about, and products) above the first &lt;code&gt;h1&lt;/code&gt;. For example, in the home route:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/')({ component: Home })

function Home() {
  return (
    &amp;lt;div className=&quot;p-8&quot;&amp;gt;
      &amp;lt;div&amp;gt;
        &amp;lt;div className=&quot;flex items-center justify-between p-8&quot;&amp;gt;
          &amp;lt;h1 className=&quot;uppercase font-bold text-2xl&quot;&amp;gt;Logo&amp;lt;/h1&amp;gt;
          &amp;lt;p&amp;gt;This is our navigtion menu&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;h1 className=&quot;font-bold text-5xl text-slate-700&quot;&amp;gt;Home&amp;lt;/h1&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should now see the header on every page. As a reminder from the previous post, the home page is &lt;a rel=&quot;nofollow&quot; href=&quot;http://localhost:3000&quot;&gt;http://localhost:3000&lt;/a&gt;, and for every other page you add a forward slash and the page name, like &lt;code&gt;/about&lt;/code&gt; and &lt;code&gt;/products&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We have a site header. But this isn't a good implementation. Remember the misspelled word? To fix it, we'd have to visit every route and correct it. That's not efficient. Delete the header from every page and let's try something better.&lt;/p&gt;
&lt;h3 id=&quot;approach-2-use-the-mainheader-component&quot;&gt;Approach 2: use the MainHeader component&lt;/h3&gt;
&lt;p&gt;Our site should be back to how it was, without a header. This time, import the &lt;code&gt;MainHeader&lt;/code&gt; component in every route and place it above the first &lt;code&gt;h1&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import MainHeader from '@/components/MainHeader'
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/about')({
  component: RouteComponent,
})

function RouteComponent() {
  return (
    &amp;lt;div className=&quot;p-8&quot;&amp;gt;
      &amp;lt;MainHeader /&amp;gt;
      &amp;lt;h1 className=&quot;font-bold text-5xl text-slate-700&quot;&amp;gt;About&amp;lt;/h1&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now go into &lt;code&gt;MainHeader.tsx&lt;/code&gt; and fix &quot;navigtion&quot; to &quot;navigation&quot;. One fix, one file, and it shows up across the app. That's a real improvement.&lt;/p&gt;
&lt;p&gt;It's still not ideal, though, because we have to remember to add &lt;code&gt;MainHeader&lt;/code&gt; to every route component we create.&lt;/p&gt;
&lt;h3 id=&quot;approach-3-put-the-header-in-root-tsx&quot;&gt;Approach 3: put the header in &lt;code&gt;__root.tsx&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;There's a way to call the header once and have it available to every route. Remember &lt;code&gt;__root.tsx&lt;/code&gt;? It wraps our entire app using the &lt;code&gt;children&lt;/code&gt; prop. What if we place the header above &lt;code&gt;children&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;First, remove &lt;code&gt;&amp;lt;MainHeader /&amp;gt;&lt;/code&gt; from your route components. Then update the root:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Only the code relevant to this discussion is shown
import MainHeader from '@/components/MainHeader'

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    &amp;lt;html lang=&quot;en&quot;&amp;gt;
      &amp;lt;head&amp;gt;
        &amp;lt;HeadContent /&amp;gt;
      &amp;lt;/head&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
          &amp;lt;MainHeader /&amp;gt;
          {children}
        &amp;lt;/QueryClientProvider&amp;gt;
        &amp;lt;Scripts /&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Visit the home page and you'll see the logo on the left and the nav menu on the right, above the page content. Visit &lt;code&gt;/about&lt;/code&gt; and &lt;code&gt;/products&lt;/code&gt; and the header is there too.&lt;/p&gt;
&lt;p&gt;Is this great? Well, not quite. It's efficient: the component is called once and shows on every route, and changes happen in one place. The limitation is flexibility. If we want a different header depending on the page the user is on, we have to add complexity to this file. A good rule of thumb is to keep &lt;code&gt;__root.tsx&lt;/code&gt; as clean and lean as possible.&lt;/p&gt;
&lt;p&gt;So delete the header and its import from &lt;code&gt;__root.tsx&lt;/code&gt;. There's an even better way: layouts.&lt;/p&gt;
&lt;h3 id=&quot;layouts&quot;&gt;Layouts&lt;/h3&gt;
&lt;p&gt;With layouts you get the efficiency of editing one file and having the change ripple through your app, but you can also have different layouts for different pages and still edit just one file for each.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;src/components&lt;/code&gt;, create a &lt;code&gt;layouts&lt;/code&gt; folder. Inside it, create &lt;code&gt;AppLayout.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { Outlet } from '@tanstack/react-router'
import MainHeader from '../MainHeader'

export default function AppLayout() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;MainHeader /&amp;gt;
      &amp;lt;div className=&quot;max-w-7xl mx-auto p-4&quot;&amp;gt;
        &amp;lt;Outlet /&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The only part that might seem strange is &lt;code&gt;Outlet&lt;/code&gt;. It works the same way as the &lt;code&gt;children&lt;/code&gt; prop in &lt;code&gt;__root.tsx&lt;/code&gt;: it's a placeholder where the matched child route gets rendered. That's how a layout wraps a route.&lt;/p&gt;
&lt;h4 id=&quot;create-the-layout-route&quot;&gt;Create the layout route&lt;/h4&gt;
&lt;p&gt;In &lt;code&gt;src/routes&lt;/code&gt;, create a file called &lt;code&gt;_appLayout.tsx&lt;/code&gt;. TanStack Start will scaffold the route for you. You may see a path conflict error with the home route. Don't worry about it, it clears up once we're done. Update the file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import AppLayout from '@/components/layouts/AppLayout'
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/_appLayout')({
  component: () =&amp;gt; {
    return &amp;lt;AppLayout /&amp;gt;
  },
  notFoundComponent: () =&amp;gt; {
    return &amp;lt;p&amp;gt;This page doesn't exist!&amp;lt;/p&amp;gt;
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's what the two options do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;component&lt;/code&gt;&lt;/strong&gt;: what actually renders when a child route matches. Since this is a layout route, &lt;code&gt;AppLayout&lt;/code&gt; contains an &lt;code&gt;&amp;lt;Outlet /&amp;gt;&lt;/code&gt;, the placeholder where whichever child route matched gets rendered.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;notFoundComponent&lt;/code&gt;&lt;/strong&gt;: defines not-found handling scoped to this layout. It lets different sections of your app show contextually appropriate &quot;not found&quot; messaging instead of one generic message everywhere.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;move-your-routes-under-the-layout&quot;&gt;Move your routes under the layout&lt;/h4&gt;
&lt;p&gt;How do we add pages to this layout? Through the file names. Rename your route files like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_appLayout.index.tsx
_appLayout.about.tsx
_appLayout.products.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then remove &lt;code&gt;&amp;lt;MainHeader /&amp;gt;&lt;/code&gt; from each route component (if you haven't already), since the layout now provides it. The route path inside each file, like &lt;code&gt;createFileRoute('/_appLayout/about')&lt;/code&gt;, is updated for you by the router plugin while the dev server is running.&lt;/p&gt;
&lt;p&gt;You'll notice the whole app layout has changed. The pages are now displayed at a fixed width instead of the full page. What's happening is that the route components are now nested inside the &lt;code&gt;_appLayout&lt;/code&gt; route. You don't see &lt;code&gt;_appLayout&lt;/code&gt; in the browser's URL because the leading underscore tells TanStack Router this is a &lt;strong&gt;pathless route&lt;/strong&gt;, in other words, a layout route.&lt;/p&gt;
&lt;p&gt;To change the header, edit the header component and it renders across the app. To change the layout structure, change one file.&lt;/p&gt;
&lt;h3 id=&quot;what-if-i-need-a-different-header-for-a-specific-page-&quot;&gt;What if I need a different header for a specific page?&lt;/h3&gt;
&lt;p&gt;Layouts have you covered. In &lt;code&gt;src/components&lt;/code&gt;, create another header component called &lt;code&gt;ProductsHeader.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export default function ProductsHeader() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;div className=&quot;flex items-center justify-between p-8&quot;&amp;gt;
        &amp;lt;h1 className=&quot;uppercase font-bold text-2xl&quot;&amp;gt;Logo&amp;lt;/h1&amp;gt;
        &amp;lt;p&amp;gt;This is the Products menu&amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now create a layout component for it in &lt;code&gt;src/components/layouts&lt;/code&gt; called &lt;code&gt;ProductsLayout.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { Outlet } from '@tanstack/react-router'
import ProductsHeader from '../ProductsHeader'

export default function ProductsLayout() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;ProductsHeader /&amp;gt;
      &amp;lt;div className=&quot;max-w-7xl mx-auto p-4&quot;&amp;gt;
        &amp;lt;Outlet /&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;ProductsHeader&lt;/code&gt; is now part of &lt;code&gt;ProductsLayout&lt;/code&gt;. To make the products route a child of this layout, rename its file from &lt;code&gt;_appLayout.products.tsx&lt;/code&gt; to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_productsLayout.products.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, in &lt;code&gt;src/routes&lt;/code&gt;, create &lt;code&gt;_productsLayout.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import ProductsLayout from '@/components/layouts/ProductsLayout'
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/_productsLayout')({
  component: () =&amp;gt; {
    return &amp;lt;ProductsLayout /&amp;gt;
  },
  notFoundComponent: () =&amp;gt; {
    return &amp;lt;p&amp;gt;This page doesn't exist!&amp;lt;/p&amp;gt;
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check the app. The home and about pages share the same header, while the products page has a different one. You could just as easily give the products page an entirely different layout structure, not just a different header.&lt;/p&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;Here's what we covered:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pasting a header into every route&lt;/strong&gt; works, but a single typo means editing every page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A reusable component&lt;/strong&gt; fixes the typo problem, but you still have to add it to every route by hand.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Putting the header in &lt;code&gt;__root.tsx&lt;/code&gt;&lt;/strong&gt; shows it everywhere from one place, but it makes your root file busier, and it's inflexible when pages need different headers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layouts&lt;/strong&gt; give you the best of both: a pathless layout route (a file starting with &lt;code&gt;_&lt;/code&gt;) renders a layout component, and that component uses &lt;code&gt;&amp;lt;Outlet /&amp;gt;&lt;/code&gt; to render whichever child route matched.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A few things to remember:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The underscore prefix makes a route pathless, so it wraps its children without adding to the URL.&lt;/li&gt;
&lt;li&gt;A route file's name decides which layout it lives under, for example &lt;code&gt;_appLayout.about.tsx&lt;/code&gt; versus &lt;code&gt;_productsLayout.products.tsx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You can create as many layouts as your app needs, each with its own header, structure, and not-found handling, and still only edit one file per layout.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the next post we'll keep building on this app. If you found this helpful, leave a comment or a reaction, and follow the series so you don't miss it.&lt;/p&gt;
&lt;p&gt;Happy coding! 🚀&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28879/tanstack-start-layouts-explained-pathless-layout-routes-outlet</guid>
<pubDate>Mon, 28 Sep 2026 06:41:48 +0000</pubDate>
</item>
<item>
<title>TanStack Start Basics Part 2 — Connecting to Your Directus Backend</title>
<link>https://coderlegion.com/26763/tanstack-start-basics-part-2-connecting-to-your-directus-backend</link>
<description>&lt;p&gt;Our frontend has been static so far — just hardcoded headings on a few pages. Today we connect it to the Directus backend built earlier in this series, using the official Directus SDK to pull real product data into our TanStack Start app.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=BUULSpRQiAY&quot;&gt;Youtube Video - Tanstack Start Basics Part 2&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;installing-the-directus-sdk&quot;&gt;Installing the Directus SDK&lt;/h3&gt;
&lt;p&gt;There are a few ways to talk to Directus from a frontend — the REST API directly, GraphQL — but the SDK is the cleanest option, and what we're using here.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @directus/sdk
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;creating-the-directus-helper&quot;&gt;Creating the Directus Helper&lt;/h3&gt;
&lt;p&gt;Inside &lt;code&gt;src/lib&lt;/code&gt;, create a new file: &lt;code&gt;directus.ts&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { createDirectus, rest } from '@directus/sdk'

export const directus = createDirectus('Your_Directus_Project_URL').with(rest())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;createDirectus&lt;/code&gt; initializes a connection client between your app and your Directus instance. On its own it starts empty — capabilities get added by chaining &lt;code&gt;.with()&lt;/code&gt;. Since we're fetching over standard HTTP REST requests, we pair it with the &lt;code&gt;rest()&lt;/code&gt; modifier.&lt;/p&gt;
&lt;h3 id=&quot;defining-our-first-type&quot;&gt;Defining Our First Type&lt;/h3&gt;
&lt;p&gt;Inside &lt;code&gt;src&lt;/code&gt;, create &lt;code&gt;types/index.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export interface Products {
  id: string
  name: string
  brief: string
  description: string
  price: number
  sale: number
  quantity: number
  isOnSale: boolean
  category: string
  image: string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;writing-the-fetch-function&quot;&gt;Writing the Fetch Function&lt;/h3&gt;
&lt;p&gt;Back in &lt;code&gt;directus.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { createDirectus, rest, readItems } from '@directus/sdk'
import type { Products } from '@/types'

export const directus = createDirectus('Your_Directus_Project_URL').with(rest())

export async function getProducts(): Promise&amp;lt;Products[]&amp;gt; {
  return directus.request(
    readItems('products', {
      fields: [
        'id',
        'name',
        'brief',
        'description',
        'price',
        'sale',
        'quantity',
        'isOnSale',
        'category',
        'image',
      ],
    }),
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;readItems&lt;/code&gt; is a built-in SDK helper that generates a request for fetching multiple items from a given collection. At this point, TypeScript throws:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Type 'Record&amp;lt;string, any&amp;gt;[]' is not assignable to type 'Products[]'.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;fixing-the-schema-error&quot;&gt;Fixing the Schema Error&lt;/h3&gt;
&lt;p&gt;This happens because the SDK doesn't automatically know your database's actual shape — it needs a &lt;code&gt;Schema&lt;/code&gt; type passed into &lt;code&gt;createDirectus&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;type Schema = {
  products: Products[]
}

export const directus = createDirectus&amp;lt;Schema&amp;gt;('Your_Directus_Project_URL').with(rest())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This declares that a &lt;code&gt;products&lt;/code&gt; collection exists and maps to an array of &lt;code&gt;Products&lt;/code&gt;. With this in place, the error resolves — and as a bonus, TypeScript now validates and autocompletes the &lt;code&gt;fields&lt;/code&gt; array against this type too.&lt;/p&gt;
&lt;h3 id=&quot;fetching-the-data-method-1-tanstack-query&quot;&gt;Fetching the Data — Method 1: TanStack Query&lt;/h3&gt;
&lt;p&gt;Install it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm i @tanstack/react-query
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Set it up in &lt;code&gt;__root.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

// Create a client
const queryClient = new QueryClient()

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    &amp;lt;html lang=&quot;en&quot;&amp;gt;
      &amp;lt;head&amp;gt;
        &amp;lt;HeadContent /&amp;gt;
      &amp;lt;/head&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
          {children}
        &amp;lt;/QueryClientProvider&amp;gt;
        &amp;lt;Scripts /&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Why does &lt;code&gt;RootDocument&lt;/code&gt; take a &lt;code&gt;children&lt;/code&gt; prop, and why wrap it?&lt;/strong&gt; &lt;code&gt;__root.tsx&lt;/code&gt; wraps your entire application — every route (About, Index, Products) gets passed in as &lt;code&gt;children&lt;/code&gt;, rendered inside this HTML shell. Wrapping &lt;code&gt;children&lt;/code&gt; with the query provider means every nested route and component now has access to TanStack Query.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now build the component, &lt;code&gt;src/components/Products.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { useQuery } from '@tanstack/react-query'

export default function Products() {
  const { data: products, isLoading: loading } = useQuery({
    queryKey: ['product'],
    queryFn: getProducts,
    staleTime: 5 * 60 * 1000,
    gcTime: 15 * 60 * 1000,
    refetchOnWindowFocus: false,
    retry: 2,
    refetchOnMount: true,
  })

  if (loading) {
    return &amp;lt;div className=&quot;container mx-auto p-8&quot;&amp;gt;Loading...&amp;lt;/div&amp;gt;
  }

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;div className=&quot;container mx-auto p-8&quot;&amp;gt;
        &amp;lt;h1 className=&quot;text-4xl font-medium mb-4&quot;&amp;gt;Products&amp;lt;/h1&amp;gt;
        &amp;lt;div className=&quot;grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6&quot;&amp;gt;
          {products?.map((product) =&amp;gt; (
            &amp;lt;div key={product.id} className=&quot;border rounded-lg p-4&quot;&amp;gt;
              &amp;lt;h2 className=&quot;text-xl font-bold mb-2&quot;&amp;gt;{product.name}&amp;lt;/h2&amp;gt;
            &amp;lt;/div&amp;gt;
          ))}
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A quick rundown of the query options:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Option &lt;/th&gt;&lt;th&gt;What it does&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;staleTime&lt;/code&gt; &lt;/td&gt;&lt;td&gt;How long fetched data stays &quot;fresh&quot; (5 min) before a refetch is considered&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;gcTime&lt;/code&gt; &lt;/td&gt;&lt;td&gt;How long unused data stays cached in memory (15 min) before being cleared&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;refetchOnWindowFocus&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Whether switching tabs and back triggers a refetch (off here)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;retry&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Silent retry attempts on failure before giving up (2 here)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;refetchOnMount&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Whether the component checks for stale data and refetches on every mount (on here)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Wire it into the route:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import Products from '@/components/Products'
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/products')({
  component: RouteComponent,
})

function RouteComponent() {
  return (
    &amp;lt;div className=&quot;p-8&quot;&amp;gt;
      &amp;lt;h1 className=&quot;font-bold text-5xl text-slate-700&quot;&amp;gt;Products&amp;lt;/h1&amp;gt;
      &amp;lt;Products /&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Visit &lt;code&gt;localhost:3000/products&lt;/code&gt; — real product data, pulled live from Directus.&lt;/p&gt;
&lt;h3 id=&quot;fetching-the-data-method-2-route-loaders&quot;&gt;Fetching the Data — Method 2: Route Loaders&lt;/h3&gt;
&lt;p&gt;Loaders work a little differently, and only on routes directly — not on standalone components. Remove the &lt;code&gt;Products&lt;/code&gt; component from the route and build it in directly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { createFileRoute } from '@tanstack/react-router'
import { getProducts } from '@/lib/directus'

export const Route = createFileRoute('/products')({
  loader: async () =&amp;gt; {
    const products = await getProducts()
    return products
  },
  component: RouteComponent,
})

function RouteComponent() {
  const products = Route.useLoaderData()

  return (
    &amp;lt;div className=&quot;p-8&quot;&amp;gt;
      &amp;lt;h1 className=&quot;font-bold text-5xl text-slate-700 mb-4&quot;&amp;gt;Products&amp;lt;/h1&amp;gt;
      &amp;lt;div className=&quot;grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6&quot;&amp;gt;
        {products?.map((product) =&amp;gt; (
          &amp;lt;div key={product.id} className=&quot;border rounded-lg p-4&quot;&amp;gt;
            &amp;lt;h2 className=&quot;text-xl font-bold mb-2&quot;&amp;gt;{product.name}&amp;lt;/h2&amp;gt;
          &amp;lt;/div&amp;gt;
        ))}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The loader runs &lt;code&gt;getProducts&lt;/code&gt; before the route renders, and &lt;code&gt;Route.useLoaderData()&lt;/code&gt; pulls that resolved data straight into the component — no loading state needed, since the data's already there by the time the page shows up.&lt;/p&gt;
&lt;h3 id=&quot;which-one-should-you-use-&quot;&gt;Which One Should You Use?&lt;/h3&gt;
&lt;p&gt;Both approaches call the exact same &lt;code&gt;getProducts&lt;/code&gt; function underneath — the difference is &lt;em&gt;where&lt;/em&gt; the fetching happens and &lt;em&gt;how&lt;/em&gt; the result reaches your component.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TanStack Query&lt;/strong&gt; shines for data that changes often or benefits from caching, background refetching, and built-in loading states — great for a snappy feel on repeat visits.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Route loaders&lt;/strong&gt; fetch before the route even renders, so there's no loading spinner at all. That pairs particularly well with TanStack Start's server-side rendering model, since the data can be resolved as part of the initial render rather than after the fact.&lt;/p&gt;
&lt;h3 id=&quot;summary-1&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;In this part, we:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Installed and configured the Directus SDK&lt;/li&gt;
&lt;li&gt;Typed our data with a &lt;code&gt;Products&lt;/code&gt; interface and a &lt;code&gt;Schema&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Built our first real fetch function using &lt;code&gt;readItems&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Rendered a live products grid two different ways — TanStack Query and a route loader&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Right now we're only displaying each product's name, but this same pattern carries directly into fuller product cards next — pricing, images, sale badges, and eventually branding and tags once those relational fields come into play.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Found this useful? Follow for more Directus and TanStack Start tutorials, or check out the video version above.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26763/tanstack-start-basics-part-2-connecting-to-your-directus-backend</guid>
<pubDate>Thu, 10 Sep 2026 22:12:24 +0000</pubDate>
</item>
<item>
<title>Setting Up Our Frontend with TanStack Start</title>
<link>https://coderlegion.com/26506/setting-up-our-frontend-with-tanstack-start</link>
<description>&lt;p&gt;We've finished our backend series — a Directus instance running on a VPS managed by Coolify, backed by PostgreSQL and Redis, with a products collection already built and secured with roles and permissions.&lt;/p&gt;
&lt;p&gt;Now it's time to build a frontend to actually display that data. For this, we're using &lt;strong&gt;TanStack Start&lt;/strong&gt;, &lt;strong&gt;Tailwind CSS&lt;/strong&gt;, and &lt;strong&gt;shadcn/ui&lt;/strong&gt;. This post covers getting TanStack Start installed and running.&lt;/p&gt;
&lt;h3 id=&quot;what-is-tanstack-start-&quot;&gt;What Is TanStack Start?&lt;/h3&gt;
&lt;p&gt;Straight from the &lt;a rel=&quot;nofollow&quot; href=&quot;https://tanstack.com/start/latest/docs/framework/react/overview&quot;&gt;official docs&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;TanStack Start is a full-stack React framework powered by TanStack Router. It provides full-document SSR, streaming, server functions, client/server builds, and more. With Vite and Rsbuild support, it's ready to develop and deploy to the hosting provider or runtime you want.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In short: it offers the same kind of full-stack capability as frameworks like Next.js or Remix, built on top of TanStack Router.&lt;/p&gt;
&lt;h3 id=&quot;creating-the-project&quot;&gt;Creating the Project&lt;/h3&gt;
&lt;p&gt;First, create a folder for your app. I'm naming mine &lt;code&gt;app_fe&lt;/code&gt; — &quot;fe&quot; for front end.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Naming convention tip:&lt;/strong&gt; I use &lt;code&gt;&amp;lt;app-name&amp;gt;_fe&lt;/code&gt; for all my frontend project folders — so an e-commerce project would be &lt;code&gt;store_fe&lt;/code&gt;. Makes the folder's purpose obvious at a glance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;From inside that folder, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx @tanstack/cli@latest create .
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note the trailing dot.&lt;/strong&gt; Since we created the folder ourselves beforehand, the dot tells the CLI to install directly into the current directory. Omit the dot if you'd rather the CLI create a new folder for you.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;walking-through-the-setup-prompts&quot;&gt;Walking Through the Setup Prompts&lt;/h3&gt;
&lt;p&gt;The CLI will walk you through a series of choices:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Prompt &lt;/th&gt;&lt;th&gt;Selection&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Select Framework &lt;/td&gt;&lt;td&gt;React&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Select Toolchain &lt;/td&gt;&lt;td&gt;ESLint&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Select Deployment Adapter &lt;/td&gt;&lt;td&gt;None (self-hosting on our own VPS)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Include demo/example pages? &lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;What add-ons would you like? &lt;/td&gt;&lt;td&gt;shadcn/ui&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Initialize a new git repository? &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Continue with these settings? &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Wait for the install to finish.&lt;/p&gt;
&lt;h3 id=&quot;running-the-app&quot;&gt;Running the App&lt;/h3&gt;
&lt;p&gt;Navigate into your app directory (if you aren't already there) and run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open your browser to &lt;code&gt;http://localhost:3000&lt;/code&gt;. Congratulations — your TanStack Start app is up and running.&lt;/p&gt;
&lt;h3 id=&quot;editing-the-home-page&quot;&gt;Editing the Home Page&lt;/h3&gt;
&lt;p&gt;Open &lt;code&gt;src/routes/index.tsx&lt;/code&gt;. Delete everything inside the div, and save — your home page should now be completely blank. This file is your landing page, the first thing visitors see.&lt;/p&gt;
&lt;p&gt;Add an &lt;code&gt;h1&lt;/code&gt; tag inside the empty div:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;h1&amp;gt;Home&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save, and you'll see &quot;Home&quot; rendered on the page. Now let's add some Tailwind styling:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;h1 className=&quot;font-bold text-5xl text-slate-700&quot;&amp;gt;Home&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save again — notice Tailwind works right out of the box, no extra configuration needed.&lt;/p&gt;
&lt;h3 id=&quot;file-based-routing&quot;&gt;File-Based Routing&lt;/h3&gt;
&lt;p&gt;TanStack Start runs on TanStack Router, which uses &lt;strong&gt;file-based routing&lt;/strong&gt;. Every file added to the &lt;code&gt;routes&lt;/code&gt; folder automatically becomes a navigable page.&lt;/p&gt;
&lt;h4 id=&quot;adding-an-about-page&quot;&gt;Adding an About Page&lt;/h4&gt;
&lt;p&gt;Create &lt;code&gt;src/routes/about.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;div className=&quot;p-8&quot;&amp;gt;
  &amp;lt;h1 className=&quot;font-bold text-5xl text-slate-700&quot;&amp;gt;About&amp;lt;/h1&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save, then visit &lt;code&gt;http://localhost:3000/about&lt;/code&gt;. Notice the home page lives at &lt;code&gt;localhost:3000/&lt;/code&gt;, and the about page lives at &lt;code&gt;localhost:3000/about&lt;/code&gt; — TanStack Start handled that routing automatically, purely based on the filename.&lt;/p&gt;
&lt;h4 id=&quot;adding-a-products-page&quot;&gt;Adding a Products Page&lt;/h4&gt;
&lt;p&gt;Create &lt;code&gt;src/routes/products.tsx&lt;/code&gt; the same way:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;div className=&quot;p-8&quot;&amp;gt;
  &amp;lt;h1 className=&quot;font-bold text-5xl text-slate-700&quot;&amp;gt;Products&amp;lt;/h1&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Visit &lt;code&gt;localhost:3000/products&lt;/code&gt;, and there's your new page. That's as simple as basic routing gets — there's plenty more you can do with nested and dynamic routes, which we'll cover as it becomes relevant.&lt;/p&gt;
&lt;h3 id=&quot;what-is-that-root-tsx-file-&quot;&gt;What Is That &lt;code&gt;__root.tsx&lt;/code&gt; File?&lt;/h3&gt;
&lt;p&gt;You've probably noticed a file called &lt;code&gt;__root.tsx&lt;/code&gt; sitting in your routes folder. Here's the simplest way to think about it:&lt;/p&gt;
&lt;p&gt;Picture any large website — Amazon, YouTube, anything. Every page has a header up top and often a footer at the bottom that never change, no matter what page you're on. But the content in the middle changes depending on where you navigated.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;__root.tsx&lt;/code&gt; is the file that builds that &quot;never changes&quot; part. It's the frame around every page in your app. Every route you build — home, about, products — gets rendered &lt;em&gt;inside&lt;/em&gt; this one file. Write it once, and every page automatically inherits it.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;A word of caution:&lt;/strong&gt; this file is central to how TanStack Router works under the hood. Avoid modifying it until you understand exactly what each piece does — we'll take a closer look at it in an upcoming post, exactly when it becomes relevant to what we're building.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;summary-2&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;In this part, we:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Installed and configured a new TanStack Start project&lt;/li&gt;
&lt;li&gt;Ran it locally and confirmed Tailwind works out of the box&lt;/li&gt;
&lt;li&gt;Learned file-based routing by building About and Products pages&lt;/li&gt;
&lt;li&gt;Got introduced to &lt;code&gt;__root.tsx&lt;/code&gt;, the file behind every page in the app&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That wraps up our frontend setup. Next up, we'll start pulling real data from our Directus backend into this frontend — connecting everything we've built across this series.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Found this useful? Follow for more Directus and TanStack Start tutorials, or check out the video version above.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26506/setting-up-our-frontend-with-tanstack-start</guid>
<pubDate>Tue, 08 Sep 2026 15:45:44 +0000</pubDate>
</item>
</channel>
</rss>