diff --git a/docs/app/global.css b/docs/app/global.css
index c7ea304b..080f7fad 100644
--- a/docs/app/global.css
+++ b/docs/app/global.css
@@ -7,6 +7,8 @@
:root {
--fd-nav-height: 56px;
+ --fd-banner-height: 0px;
+ --fd-tocnav-height: 0px;
--background: oklch(1 0 0);
@@ -255,15 +257,19 @@
}
html {
- scroll-behavior: smooth;
+ scroll-behavior: auto;
scroll-padding-top: calc(
- var(--fd-nav-height) +
+ var(--fd-nav-height, 56px) +
var(--fd-banner-height, 0px) +
var(--fd-tocnav-height, 0px) +
- 16px
+ 24px
);
}
+html:not([data-anchor-scrolling]) {
+ scroll-behavior: smooth;
+}
+
/* Global, accessible custom scrollbars */
* {
scrollbar-width: thin; /* Firefox */
diff --git a/docs/app/layout.tsx b/docs/app/layout.tsx
index 8c7e699f..2bc730e5 100644
--- a/docs/app/layout.tsx
+++ b/docs/app/layout.tsx
@@ -10,6 +10,7 @@ import { Analytics } from "@vercel/analytics/react";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner";
import { CustomSearchDialog } from "@/components/search-dialog";
+import { AnchorScroll } from "@/components/anchor-scroll-fix";
export const metadata = createMetadata({
title: {
@@ -58,6 +59,7 @@ export default function Layout({ children }: { children: ReactNode }) {
: undefined,
}}
>
+
{children}
diff --git a/docs/components/anchor-scroll-fix.tsx b/docs/components/anchor-scroll-fix.tsx
new file mode 100644
index 00000000..df4e5289
--- /dev/null
+++ b/docs/components/anchor-scroll-fix.tsx
@@ -0,0 +1,107 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+
+export function AnchorScroll() {
+ const scrollTimeoutRef = useRef(undefined);
+ const isScrollingRef = useRef(false);
+
+ useEffect(() => {
+ function calculateScrollOffset() {
+ const root = document.documentElement;
+ const navHeight = parseInt(
+ getComputedStyle(root).getPropertyValue("--fd-nav-height") || "56",
+ );
+ const bannerHeight = parseInt(
+ getComputedStyle(root).getPropertyValue("--fd-banner-height") || "0",
+ );
+ const tocnavHeight = parseInt(
+ getComputedStyle(root).getPropertyValue("--fd-tocnav-height") || "0",
+ );
+
+ return navHeight + bannerHeight + tocnavHeight + 24;
+ }
+
+ function smoothScrollToElement(element: HTMLElement) {
+ if (isScrollingRef.current) return;
+
+ isScrollingRef.current = true;
+ document.documentElement.setAttribute("data-anchor-scrolling", "true");
+
+ const elementRect = element.getBoundingClientRect();
+ const scrollOffset = calculateScrollOffset();
+ const targetPosition =
+ window.pageYOffset + elementRect.top - scrollOffset;
+
+ // Simple smooth scroll animation
+ const startPosition = window.pageYOffset;
+ const distance = targetPosition - startPosition;
+ const duration = Math.min(500, Math.abs(distance) * 0.3);
+ const startTime = performance.now();
+
+ function animateScroll(currentTime: number) {
+ const elapsed = currentTime - startTime;
+ const progress = Math.min(elapsed / duration, 1);
+ const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);
+ const currentPosition =
+ startPosition + distance * easeOutCubic(progress);
+
+ window.scrollTo(0, currentPosition);
+
+ if (progress < 1) {
+ requestAnimationFrame(animateScroll);
+ } else {
+ document.documentElement.removeAttribute("data-anchor-scrolling");
+ isScrollingRef.current = false;
+ }
+ }
+
+ requestAnimationFrame(animateScroll);
+ }
+
+ function handleAnchorScroll() {
+ if (window.location.hash) {
+ const element = document.getElementById(window.location.hash.slice(1));
+ if (element) {
+ scrollTimeoutRef.current = setTimeout(
+ () => smoothScrollToElement(element),
+ 100,
+ );
+ }
+ }
+ }
+
+ function handleHashChange() {
+ const element = document.getElementById(window.location.hash.slice(1));
+ if (element) smoothScrollToElement(element);
+ }
+
+ function handleAnchorClick(event: Event) {
+ const link = (event.target as HTMLElement).closest(
+ 'a[href^="#"]',
+ ) as HTMLAnchorElement;
+
+ if (link?.hash) {
+ event.preventDefault();
+ const element = document.getElementById(link.hash.slice(1));
+
+ if (element) {
+ history.pushState(null, "", link.hash);
+ smoothScrollToElement(element);
+ }
+ }
+ }
+
+ handleAnchorScroll();
+ window.addEventListener("hashchange", handleHashChange);
+ document.addEventListener("click", handleAnchorClick);
+
+ return () => {
+ if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);
+ window.removeEventListener("hashchange", handleHashChange);
+ document.removeEventListener("click", handleAnchorClick);
+ };
+ }, []);
+
+ return null;
+}
diff --git a/docs/components/docs/layout/nav.tsx b/docs/components/docs/layout/nav.tsx
index 23ad1880..43aa49f2 100644
--- a/docs/components/docs/layout/nav.tsx
+++ b/docs/components/docs/layout/nav.tsx
@@ -48,11 +48,14 @@ export function NavProvider({
if (transparentMode !== "top") return;
const listener = () => {
+ if (document.documentElement.hasAttribute("data-anchor-scrolling")) {
+ return;
+ }
setTransparent(window.scrollY < 10);
};
listener();
- window.addEventListener("scroll", listener);
+ window.addEventListener("scroll", listener, { passive: true });
return () => {
window.removeEventListener("scroll", listener);
};