+
{display === 'grid' ? (
@@ -149,7 +149,7 @@ const InfiniteHits = observer(({ state$ }: InfiniteHitsProps) => {
)}
-
+
);
});
diff --git a/website/app/components/search/ScrollToTop.module.css b/website/app/components/search/ScrollToTop.module.css
new file mode 100644
index 0000000000..4843c9918a
--- /dev/null
+++ b/website/app/components/search/ScrollToTop.module.css
@@ -0,0 +1,31 @@
+.button {
+ position: fixed;
+
+ bottom: 30px;
+ right: 55px;
+
+ height: 56px;
+ width: 56px;
+ border-radius: 48px;
+
+ @media (min-width: $mantine-breakpoint-xs) {
+ right: 60px;
+ }
+
+ @media (min-width: $mantine-breakpoint-sm) {
+ bottom: 24px;
+ right: 56px;
+ }
+
+ @media (min-width: $mantine-breakpoint-md) {
+ right: 56px;
+ }
+
+ @media (min-width: $mantine-breakpoint-lg) {
+ right: 100px;
+ }
+
+ @media (min-width: $mantine-breakpoint-xl) {
+ right: 80px;
+ }
+}
diff --git a/website/app/components/search/ScrollToTop.tsx b/website/app/components/search/ScrollToTop.tsx
new file mode 100644
index 0000000000..23d52f01e1
--- /dev/null
+++ b/website/app/components/search/ScrollToTop.tsx
@@ -0,0 +1,65 @@
+import { ActionIcon, Portal, Transition } from '@mantine/core';
+import { useEffect, useState } from 'react';
+
+import { IconUp } from '../icons/Up';
+import classes from './ScrollToTop.module.css';
+
+interface ScrollToTopProps {
+ containerId: string;
+ targetRef: React.RefObject;
+}
+
+const scrollUp = () => {
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+};
+
+const ScrollToTop = ({ containerId, targetRef }: ScrollToTopProps) => {
+ const [showButton, setShowButton] = useState(false);
+
+ useEffect(() => {
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ setShowButton(!entry.isIntersecting);
+ },
+ { threshold: 0.5 },
+ );
+
+ const targetNode = targetRef.current;
+ if (targetNode) {
+ observer.observe(targetNode);
+ }
+
+ return () => {
+ if (targetNode) {
+ observer.unobserve(targetNode);
+ }
+ };
+ }, [targetRef]);
+
+ return (
+
+ {(styles) => (
+
+
+
+
+
+ )}
+
+ );
+};
+
+export { ScrollToTop };
diff --git a/website/app/routes/_index.tsx b/website/app/routes/_index.tsx
index a8a64cc5fa..27b97acb0d 100644
--- a/website/app/routes/_index.tsx
+++ b/website/app/routes/_index.tsx
@@ -8,6 +8,7 @@ import { type UiState } from 'instantsearch.js';
// @ts-expect-error - No type definitions available
import { history } from 'instantsearch.js/cjs/lib/routers/index.js';
import { type BrowserHistoryArgs } from 'instantsearch.js/es/lib/routers/history';
+import { useRef } from 'react';
import { renderToString } from 'react-dom/server';
import {
getServerState,
@@ -19,6 +20,7 @@ import {
import { Filters } from '@/components/search/Filters';
import { InfiniteHits } from '@/components/search/Hits';
import { type SearchObject } from '@/components/search/observables';
+import { ScrollToTop } from '@/components/search/ScrollToTop';
import classes from '@/styles/global.module.css';
import { getSSRCache, setSSRCache } from '@/utils/algolia.server';
@@ -165,6 +167,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
export default function Index() {
const { serverState, serverUrl } = useLoaderData();
+ const searchRef = useRef(null);
const state$ = useObservable({
size: 32,
@@ -195,12 +198,13 @@ export default function Index() {
future={{ preserveSharedStateOnUnmount: true }}
>
-
+
+
diff --git a/website/public/icons/up.svg b/website/public/icons/up.svg
new file mode 100644
index 0000000000..8e0c13b9d9
--- /dev/null
+++ b/website/public/icons/up.svg
@@ -0,0 +1,5 @@
+