diff --git a/apps/www/components/ui/heading.tsx b/apps/www/components/ui/heading.tsx
index 6d819e7fb..033c6cede 100644
--- a/apps/www/components/ui/heading.tsx
+++ b/apps/www/components/ui/heading.tsx
@@ -2,6 +2,7 @@
import type { ComponentProps } from "react";
import { useEffect, useState } from "react";
+import { useIsMobile } from "../../hooks/use-is-mobile";
// Global state to track the active heading ID
let activeHeadingId: string | null = null;
@@ -24,18 +25,27 @@ export function Heading({
as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
}) {
const [isActive, setIsActive] = useState(false);
+ const isMobile = useIsMobile();
useEffect(() => {
const handleActiveChange = (activeId: string | null) => {
- setIsActive(activeId === id);
+ // Only set active state on mobile devices
+ if (isMobile) {
+ setIsActive(activeId === id);
+ }
};
headingListeners.add(handleActiveChange);
- // Set initial state
- setIsActive(activeHeadingId === id);
+ // Set initial state only on mobile
+ if (isMobile) {
+ setIsActive(activeHeadingId === id);
+ }
const handleClickOutside = (event: MouseEvent) => {
+ // Only handle click outside on mobile devices
+ if (!isMobile) return;
+
// Check if the click is outside any heading or anchor
const target = event.target as Element;
if (
@@ -52,11 +62,15 @@ export function Heading({
headingListeners.delete(handleActiveChange);
document.removeEventListener("click", handleClickOutside);
};
- }, [id]);
+ }, [id, isMobile]);
const handleAnchorClick = (e: React.MouseEvent) => {
e.preventDefault();
- setActiveHeading(id ?? null);
+
+ // Only set active state on mobile devices
+ if (isMobile) {
+ setActiveHeading(id ?? null);
+ }
// Always scroll to the element, even if hash is already set
const element = document.getElementById(id || "");
@@ -74,8 +88,10 @@ export function Heading({
return;
}
- // Show the anchor but don't scroll - only the pound symbol should scroll
- setActiveHeading(id ?? null);
+ // Only show the anchor on mobile - on desktop it will only show on hover
+ if (isMobile) {
+ setActiveHeading(id ?? null);
+ }
};
if (!id)
@@ -95,15 +111,17 @@ export function Heading({
- #
+ Link to section: {id}
+ #
{children}
diff --git a/apps/www/hooks/use-is-mobile.ts b/apps/www/hooks/use-is-mobile.ts
new file mode 100644
index 000000000..13d9cd9f2
--- /dev/null
+++ b/apps/www/hooks/use-is-mobile.ts
@@ -0,0 +1,27 @@
+import * as React from "react";
+
+const MOBILE_BREAKPOINT = 768;
+
+/**
+ * Hook to detect if the current viewport is mobile-sized.
+ *
+ * Based on the implementation from Vercel AI Chatbot:
+ * @see https://github.com/vercel/ai-chatbot/blob/main/hooks/use-mobile.tsx
+ */
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(
+ undefined,
+ );
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ };
+ mql.addEventListener("change", onChange);
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ return () => mql.removeEventListener("change", onChange);
+ }, []);
+
+ return !!isMobile;
+}