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