diff --git a/package.json b/package.json
index 1ddedfcd9d..1caacdcb5f 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
+ "@radix-ui/react-toggle": "^1.1.0",
+ "@radix-ui/react-toggle-group": "^1.1.0",
"@t3-oss/env-nextjs": "^0.11.1",
"@tanstack/react-query": "^5.60.2",
"@tanstack/react-query-devtools": "^5.61.0",
diff --git a/src/app/explorer/_components/StickyBar.tsx b/src/app/explorer/_components/StickyBar.tsx
new file mode 100644
index 0000000000..c551ef5ee4
--- /dev/null
+++ b/src/app/explorer/_components/StickyBar.tsx
@@ -0,0 +1,125 @@
+"use client";
+
+import { AuthBoundary } from "@/components/AuthBoundary";
+//import { useWeb3ConnectionManager } from "@/components/Web3ConnectionManager/hooks/useWeb3ConnectionManager"
+import { buttonVariants } from "@/components/ui/Button";
+import { ToggleGroup, ToggleGroupItem } from "@/components/ui/ToggleGroup";
+import useIsStuck from "@/hooks/useIsStuck";
+import useScrollspy from "@/hooks/useScrollSpy";
+import { cn } from "@/lib/cssUtils";
+import { Plus } from "@phosphor-icons/react";
+import { useAtom, useAtomValue, useSetAtom } from "jotai";
+import Link from "next/link";
+import { useEffect } from "react";
+import { activeSectionAtom, isNavStuckAtom, isSearchStuckAtom } from "../atoms";
+import { ACTIVE_SECTION } from "../constants";
+
+export const smoothScrollTo = (id: string) => {
+ const target = document.getElementById(id);
+
+ if (!target) return;
+
+ window.scrollTo({
+ behavior: "smooth",
+ top: target.offsetTop,
+ });
+};
+
+const Nav = () => {
+ const isNavStuck = useAtomValue(isNavStuckAtom);
+ const isSearchStuck = useAtomValue(isSearchStuckAtom);
+ const [activeSection, setActiveSection] = useAtom(activeSectionAtom);
+ const spyActiveSection = useScrollspy(Object.values(ACTIVE_SECTION), 100);
+ useEffect(() => {
+ if (!spyActiveSection) return;
+ setActiveSection(
+ spyActiveSection as (typeof ACTIVE_SECTION)[keyof typeof ACTIVE_SECTION],
+ );
+ }, [spyActiveSection, setActiveSection]);
+
+ return (
+
+ value &&
+ setActiveSection(
+ value as (typeof ACTIVE_SECTION)[keyof typeof ACTIVE_SECTION],
+ )
+ }
+ value={activeSection}
+ >
+ smoothScrollTo(ACTIVE_SECTION.yourGuilds)}
+ >
+ Your guilds
+
+ smoothScrollTo(ACTIVE_SECTION.exploreGuilds)}
+ >
+ Explore guilds
+
+
+ );
+};
+
+const CreateGuildLink = () => {
+ const isNavStuck = useAtomValue(isNavStuckAtom);
+ return (
+
+
+ Create guild
+
+ );
+};
+
+export const StickyBar = () => {
+ //const { isWeb3Connected } = useWeb3ConnectionManager()
+ const setIsNavStuck = useSetAtom(isNavStuckAtom);
+ const isSearchStuck = useAtomValue(isSearchStuckAtom);
+ const { ref: navToggleRef } = useIsStuck(setIsNavStuck);
+
+ return (
+
+ );
+};
diff --git a/src/app/explorer/atoms.ts b/src/app/explorer/atoms.ts
index cc0ef0d59c..f354b9ac36 100644
--- a/src/app/explorer/atoms.ts
+++ b/src/app/explorer/atoms.ts
@@ -1,5 +1,9 @@
import { atom } from "jotai";
+import { ACTIVE_SECTION } from "./constants";
export const searchAtom = atom(undefined);
export const isNavStuckAtom = atom(false);
export const isSearchStuckAtom = atom(false);
+export const activeSectionAtom = atom<
+ (typeof ACTIVE_SECTION)[keyof typeof ACTIVE_SECTION]
+>(ACTIVE_SECTION.yourGuilds);
diff --git a/src/app/explorer/constants.ts b/src/app/explorer/constants.ts
index 3c0383f14b..0548d43ce2 100644
--- a/src/app/explorer/constants.ts
+++ b/src/app/explorer/constants.ts
@@ -2,4 +2,4 @@ export const PAGE_SIZE = 24;
export const ACTIVE_SECTION = {
yourGuilds: "your-guilds",
exploreGuilds: "explore-guilds",
-};
+} as const;
diff --git a/src/components/ui/Toggle.tsx b/src/components/ui/Toggle.tsx
new file mode 100644
index 0000000000..38139c9378
--- /dev/null
+++ b/src/components/ui/Toggle.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import { cn } from "@/lib/cssUtils";
+import * as TogglePrimitive from "@radix-ui/react-toggle";
+import { type VariantProps, cva } from "class-variance-authority";
+import {
+ type ComponentPropsWithoutRef,
+ type ElementRef,
+ forwardRef,
+} from "react";
+
+const toggleVariants = cva(
+ "inline-flex items-center justify-center rounded-lg text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-4 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground font-medium",
+ {
+ variants: {
+ variant: {
+ secondary:
+ "text-secondary-foreground hover:bg-secondary data-[state=on]:bg-secondary active:bg-secondary-hover",
+ primary:
+ "hover:bg-secondary-hover active:bg-secondary-active data-[state=on]:bg-primary data-[state=on]:text-primary-foreground bg-secondary text-secondary-foreground",
+ mono: "text-white hover:bg-white/10 data-[state=on]:bg-white/15 hover:text-white data-[state=on]:text-white",
+ },
+ size: {
+ sm: "h-8 px-2.5",
+ md: "h-10 px-3",
+ lg: "h-11 px-5 font-semibold text-base",
+ icon: "size-9",
+ },
+ },
+ defaultVariants: {
+ variant: "secondary",
+ size: "md",
+ },
+ },
+);
+
+const Toggle = forwardRef<
+ ElementRef,
+ ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, variant, size, ...props }, ref) => (
+
+));
+
+Toggle.displayName = TogglePrimitive.Root.displayName;
+
+export { Toggle, toggleVariants };
diff --git a/src/components/ui/ToggleGroup.tsx b/src/components/ui/ToggleGroup.tsx
new file mode 100644
index 0000000000..89f7baade1
--- /dev/null
+++ b/src/components/ui/ToggleGroup.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import { toggleVariants } from "@/components/ui/Toggle";
+import { cn } from "@/lib/cssUtils";
+import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
+import type { VariantProps } from "class-variance-authority";
+import {
+ type ComponentPropsWithoutRef,
+ type ElementRef,
+ createContext,
+ forwardRef,
+ useContext,
+} from "react";
+
+const ToggleGroupContext = createContext>({
+ size: "md",
+ variant: "secondary",
+});
+
+const ToggleGroup = forwardRef<
+ ElementRef,
+ ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, variant, size, children, ...props }, ref) => (
+
+
+ {children}
+
+
+));
+
+ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
+
+const ToggleGroupItem = forwardRef<
+ ElementRef,
+ ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, children, variant, size, ...props }, ref) => {
+ const context = useContext(ToggleGroupContext);
+
+ return (
+
+ {children}
+
+ );
+});
+
+ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
+
+export { ToggleGroup, ToggleGroupItem };
diff --git a/src/hooks/useIsStuck.ts b/src/hooks/useIsStuck.ts
new file mode 100644
index 0000000000..9497b81f35
--- /dev/null
+++ b/src/hooks/useIsStuck.ts
@@ -0,0 +1,48 @@
+import {
+ type Dispatch,
+ type MutableRefObject,
+ type SetStateAction,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+
+/**
+ * The IntersectionObserver triggers if the element is off the viewport, so we have
+ * to set top="-1px" or bottom="-1px" on the sticky element instead of 0
+ */
+const useIsStuck = (
+ setIsStuck?: Dispatch>,
+): { ref: MutableRefObject; isStuck?: boolean } => {
+ const ref = useRef(null);
+ const [isStuck, setIsStuckLocal] = useState(false);
+ const setIsStuckActive = setIsStuck ?? setIsStuckLocal;
+
+ useEffect(() => {
+ if (!ref.current) return;
+ const cachedRef = ref.current;
+ const topOffsetPx = Number.parseInt(getComputedStyle(cachedRef).top) + 1;
+ const bottomOffsetPx =
+ Number.parseInt(getComputedStyle(cachedRef).bottom) + 1;
+
+ const observer = new IntersectionObserver(
+ ([e]) => {
+ setIsStuckActive(
+ !e.isIntersecting &&
+ (e.boundingClientRect.top < topOffsetPx ||
+ e.boundingClientRect.bottom > bottomOffsetPx),
+ );
+ },
+ {
+ threshold: [1],
+ rootMargin: `-${topOffsetPx || 0}px 0px 0px ${bottomOffsetPx || 0}px`,
+ },
+ );
+ observer.observe(cachedRef);
+ return () => observer.unobserve(cachedRef);
+ }, [ref]);
+
+ return { ref, isStuck: setIsStuck ? undefined : isStuck };
+};
+
+export default useIsStuck;
diff --git a/src/hooks/useScrollSpy.ts b/src/hooks/useScrollSpy.ts
new file mode 100644
index 0000000000..7fb5b65864
--- /dev/null
+++ b/src/hooks/useScrollSpy.ts
@@ -0,0 +1,48 @@
+import { useLayoutEffect, useState } from "react";
+
+// Restrict value to be between the range [0, value]
+const clamp = (value: number) => Math.max(0, value);
+
+// Check if number is between two values
+const isBetween = (value: number, floor: number, ceil: number) =>
+ value >= floor && value <= ceil;
+
+const useScrollspy = (ids: string[], offset = 0) => {
+ const [activeId, setActiveId] = useState("");
+
+ useLayoutEffect(() => {
+ const listener = () => {
+ const scroll = window.scrollY;
+
+ const position = ids
+ .map((id) => {
+ const element = document.getElementById(id);
+
+ if (!element) return { id, top: -1, bottom: -1 };
+
+ const rect = element.getBoundingClientRect();
+ const top = clamp(rect.top + scroll - offset);
+ const bottom = clamp(rect.bottom + scroll - offset);
+
+ return { id, top, bottom };
+ })
+ .find(({ top, bottom }) => isBetween(scroll, top, bottom));
+
+ setActiveId(position?.id || "");
+ };
+
+ listener();
+
+ window.addEventListener("resize", listener);
+ window.addEventListener("scroll", listener);
+
+ return () => {
+ window.removeEventListener("resize", listener);
+ window.removeEventListener("scroll", listener);
+ };
+ }, [ids, offset]);
+
+ return activeId;
+};
+
+export default useScrollspy;