Command Palette

Search for a command to run...
HQ UI

Tanstack Start

Learn how to integrate HQ UI in Tanstack Start React Project

Create New Project

I recommend using tanstack cli to create new project rather than using shadcn installer, because tanstack cli gives you more options which package you want to install. And if you choose Shadcn in the tanstack cli installation, The component like Theme Toggle are already setup.

npx @tanstack/cli@latest create

Registry

First, add the HQ UI in your components.json:

components.json
  ...
  "registries": {
    "@hq": "https://hq-ui.vercel.app/r/{name}"
  }

Add Components

You can now start adding components to your project.

npx shadcn@latest add @hq/button

Or if you don't wan't to modify components.json, you can use this command

npx shadcn@latest add https://hq-ui.vercel.app/r/button

Client Side Routing

React Aria components such as Link, Menu, Tabs, and Table transform elements into clickable links that navigate when clicked. These components utilize the href prop to render as an <a> tag, supporting attributes like target and download. And with Tanstack Router, you'll get the autocomplete which will help you to define the routes typesafely.

Client Provider

To integrate with Tanstack Start, ensure the locale on the server matches the client, and configure React Aria links to use the Tanstack Router.

client-provider.tsx
import { type NavigateOptions, type ToOptions, useRouter, } from "@tanstack/react-router";
import { RouterProvider } from "react-aria-components";

declare module "react-aria-components" {
  interface RouterConfig {
    href: ToOptions["to"];
    routerOptions: Omit<NavigateOptions, keyof ToOptions>;
  }
}

export function ClientProvider({ children }: { children: React.ReactNode }) {
  const router = useRouter();

  return (
    <RouterProvider
      navigate={(to, options) => router.navigate({ to, ...options })}
    >
      {children}
    </RouterProvider>
  );
}

Then use in your __root.tsx file.

__root.tsx
import { ClientProvider } from "#/components/client-provider"

...
function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      //
      <ClientProvider>
        {children}
      </ClientProvider>
      //
    </html>
  )
}

Dark Mode

Theme Provider

If you are using the Tanstack CLI, Theme Provider is already setup. You can skip this step

If the theme-provider isn't setup, or you want to create your own then you have to create one.

npm i @tabler/icons-react
components/theme-toggle.tsx
import { IconDeviceDesktop, IconMoon, IconSun } from "@tabler/icons-react";
import { useEffect, useState } from "react";
import { Button } from "#/components/ui/button";

type ThemeMode = "light" | "dark" | "auto";

function getInitialMode(): ThemeMode {
	if (typeof window === "undefined") {
		return "auto";
	}

	const stored = window.localStorage.getItem("theme");
	if (stored === "light" || stored === "dark" || stored === "auto") {
		return stored;
	}

	return "auto";
}

function applyThemeMode(mode: ThemeMode) {
	const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
	const resolved = mode === "auto" ? (prefersDark ? "dark" : "light") : mode;

	document.documentElement.classList.remove("light", "dark");
	document.documentElement.classList.add(resolved);

	if (mode === "auto") {
		document.documentElement.removeAttribute("data-theme");
	} else {
		document.documentElement.setAttribute("data-theme", mode);
	}

	document.documentElement.style.colorScheme = resolved;
}

export default function ThemeToggle() {
	const [mode, setMode] = useState<ThemeMode>("auto");

	useEffect(() => {
		const initialMode = getInitialMode();
		setMode(initialMode);
		applyThemeMode(initialMode);
	}, []);

	useEffect(() => {
		if (mode !== "auto") {
			return;
		}

		const media = window.matchMedia("(prefers-color-scheme: dark)");
		const onChange = () => applyThemeMode("auto");

		media.addEventListener("change", onChange);
		return () => {
			media.removeEventListener("change", onChange);
		};
	}, [mode]);

	function toggleMode() {
		const nextMode: ThemeMode =
			mode === "light" ? "dark" : mode === "dark" ? "auto" : "light";
		setMode(nextMode);
		applyThemeMode(nextMode);
		window.localStorage.setItem("theme", nextMode);
	}

	return (
		<Button
			aria-label={`Switch to ${mode}` === "light" ? "dark" : "light" + "mode"}
			onPress={toggleMode}
			size="icon"
			variant="outline"
		>
			{mode === "auto" ? (
				<IconDeviceDesktop
					aria-hidden
					className="absolute rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
				/>
			) : (
				<>
					<IconSun
						aria-hidden
						className="absolute rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
					/>
					<IconMoon
						aria-hidden
						className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
					/>
				</>
			)}
		</Button>
	);
}