A customizable section picker for navigating between content sections.
import SectionNavigator from "@/registry/components/section-navigator/section-navigator";
const SectionNavigatorPreview = () => {
/** Dynamically update this with the active section based on scroll position */
const activeSectionId = sections[2].id;
return (
<>
<SectionNavigator
sections={sections}
activeSectionId={activeSectionId}
triggerButtonClassName="backdrop-blur-md"
/>
<p className="p-5 text-zinc-500">
Discover sections from the menu on the right.
</p>
</>
);
};
const sections = [
{ id: "id-0", name: "What should I cook with these ingredients?" },
{ id: "id-1", name: "Help me plan a birthday party." },
{ id: "id-2", name: "Can you recommend movies based on my favorites?" },
{ id: "id-3", name: "Can you summarize this long article/document and highlight the important parts?" },
{ id: "id-4", name: "Can you create a step-by-step plan for learning a new skill?" },
{ id: "id-5", name: "I want to buy a new phone. What should I consider first?" },
{ id: "id-6", name: "Should I prioritize RAM, processor, camera, or storage?", },
{ id: "id-7", name: "Is camera quality or performance more important for my usage?" },
{ id: "id-8", name: "Help me choose between these two phones." },
{ id: "id-9", name: "Will this phone receive updates for long enough?" },
{ id: "id-10", name: "What accessories should I get?" },
{ id: "id-11", name: "Should I buy now or wait for a new release?" },
{ id: "id-12", name: "Which one would you personally recommend for my needs?" },
];
export default SectionNavigatorPreview; Install the following packages before using this component.
import cn from "@/utils/cn";
import { memo, useCallback, useState, type MouseEventHandler } from "react";
import { createPortal } from "react-dom";
import { motion } from "motion/react";
export type SectionEntry = {
id: string,
name: string,
className?: string;
};
export type SectionNavigatorProps = {
sections: SectionEntry[],
position?: "left" | "right" | "top" | "bottom",
activeSectionId?: SectionEntry["id"],
onSectionClick?: Function,
className?: string;
triggerButtonClassName?: string;
sectionListClassName?: string;
sectionListItemClassName?: string;
} & React.ComponentProps<"div">;
type SectionListProps = {
onSectionEntryClick: Function,
onClose: MouseEventHandler<HTMLDivElement | HTMLUListElement>,
} & Pick<SectionNavigatorProps, (
"sections" |
"position" |
"activeSectionId" |
"sectionListClassName" |
"sectionListItemClassName"
)>;
const positionClassConfig = {
"right": "right-[16px] top-[50%] translate-y-[-50%]",
"left": "left-[16px] top-[50%] translate-y-[-50%]",
"top": "top-[16px] left-[50%] translate-x-[-50%]",
"bottom": "bottom-[16px] left-[50%] translate-x-[-50%]",
};
const SectionNavigator = (
props: SectionNavigatorProps,
) => {
const {
sections,
position = "right",
activeSectionId,
onSectionClick,
className,
triggerButtonClassName,
sectionListClassName,
sectionListItemClassName,
...restProps
} = props;
const [showNavigator, setShowNavigator] = useState(false);
const navigatorOpenHandler = useCallback(() => {
setShowNavigator(true);
}, []);
const navigatorCloseHandler = useCallback(() => {
setShowNavigator(false);
}, []);
const handleSectionEntryClick = (
section: SectionEntry,
) => {
setShowNavigator(false);
onSectionClick?.(section);
};
return (
<div
{...restProps}
className={cn(
"fixed z-[10]",
positionClassConfig[position],
className,
)}
>
{
showNavigator ? (
<SectionList
sections={sections}
position={position}
activeSectionId={activeSectionId}
onSectionEntryClick={handleSectionEntryClick}
onClose={navigatorCloseHandler}
sectionListClassName={sectionListClassName}
/>
) : (
<motion.button
className={cn(
"p-2 flex gap-[6px] rounded-md",
{
"flex-col": ["left", "right"].includes(position),
},
triggerButtonClassName,
)}
onMouseOver={navigatorOpenHandler}
onFocus={navigatorOpenHandler}
onTap={navigatorOpenHandler}
>
{sections.map((sectionEntry) => (
<span
key={sectionEntry.id}
className={cn(
"bg-gray-900/25 dark:bg-gray-100/25 rounded-md",
{
"w-4 h-[2px]": ["left", "right"].includes(position),
"w-[2px] h-4": ["top", "bottom"].includes(position),
"bg-gray-900 dark:bg-gray-100": sectionEntry.id === activeSectionId,
}
)}
/>
))}
</motion.button>
)
}
</div>
);
};
const SectionList = memo((
props: SectionListProps,
) => {
const {
sections,
position = "right",
sectionListClassName,
sectionListItemClassName,
activeSectionId,
onSectionEntryClick,
onClose,
} = props;
return createPortal(
<>
<div
aria-hidden={true}
className="fixed inset-0 z-9"
onClick={onClose}
/>
<motion.ul
className={cn(
"fixed z-10",
"w-xs max-h-[400px]",
"p-2 grid gap-1 overflow-y-auto rounded-xl",
"bg-white dark:bg-neutral-700",
"shadow shadow-md",
positionClassConfig[position],
sectionListClassName,
)}
onMouseLeave={onClose}
style={{
scale: 0.25,
opacity: 0,
transformOrigin: ({
"left": "0% 50%",
"right": "100% 50%",
"top": "50% 0%",
"bottom": "50% 100%",
})[position]
}}
animate={{
scale: 1,
opacity: 1,
}}
transition={{
type: "spring",
stiffness: 200,
damping: 15,
mass: 0.8,
}}
>
{sections.map((sectionEntry, sectionEntryIndex) => (
<li
key={sectionEntry.id}
className="truncate"
>
<motion.button
className={cn(
"py-[6px] px-2 w-full rounded-md",
"text-neutral-800 dark:text-neutral-200 text-sm text-left truncate",
"hover:bg-neutral-300/50 dark:hover:bg-neutral-800/50",
"cursor-pointer",
{
"bg-neutral-300 dark:bg-neutral-800": (
sectionEntry.id === activeSectionId
),
},
sectionListItemClassName,
sectionEntry.className,
)}
animate={{
opacity: 1,
y: 0,
filter: "blur(0px)",
}}
style={{
opacity: 0,
y: 20,
filter: "blur(2px)",
}}
transition={{
delay: 0.05 + (0.025 * (sectionEntryIndex)),
}}
title={sectionEntry.name}
onTap={() => onSectionEntryClick(sectionEntry)}
>
{sectionEntry.name}
</motion.button>
</li>
))}
</motion.ul>
</>,
document.body
);
});
export default memo(SectionNavigator); | Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| sections | Array<{ id: string; name: string; className?: string }> | Yes | — | List of sections displayed in the navigator. Each section requires a unique id and display name, with an optional className for custom styling. |
| position | "left" | "right" | "top" | "bottom" | No | "right" | Defines the position/orientation of the section navigator. |
| activeSectionId | string | No | — | ID of the currently visible section. |
| onSectionClick | (section: { id: string; name: string; className?: string }) => void | No | — | Callback fired when a section is clicked. Returns the selected section object. |
| className | string | No | — | Custom CSS classes applied to the root navigator container. |
| triggerButtonClassName | string | No | — | Custom CSS classes applied to the main trigger button that opens the section list. |
| sectionListClassName | string | No | — | Custom CSS classes applied to the section list wrapper. |
| sectionListItemClassName | string | No | — | Custom CSS classes applied to individual section list items. |
If you find this component useful, consider starring the repository on GitHub. Found a bug or have a suggestion? Open an issue to help improve it.