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.
Registry
First, add the HQ UI in your components.json:
...
"registries": {
"@hq": "https://hq-ui.vercel.app/r/{name}"
}Add Components
You can now start adding components to your project.
Or if you don't wan't to modify components.json, you can use this command
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.
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.
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.
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>
);
}