elements
lifecycle
browser
- useAudio
- useBattery
- useBluetooth
- useBreakpoints
- useBroadcastChannel
- useBrowserLocation
- useClipboard
- useCopy
- useCssVar
- useDisplayMedia
- useDocumentEvent
- useDocumentTitle
- useDocumentVisibility
- useEventListener
- useEventSource
- useEyeDropper
- useFavicon
- useFileSystemAccess
- useFps
- useFullscreen
- useGamepad
- useGeolocation
- useMeasure
- useMediaControls
- useMediaQuery
- useMemory
- useNetwork
- useObjectUrl
- useOnline
- useOtpCredential
- usePermission
- usePictureInPicture
- usePointerLock
- usePostMessage
- useRaf
- useShare
- useSpeechRecognition
- useSpeechSynthesis
- useSticky
- useVibrate
- useVirtualKeyboard
- useWakeLock
- useWebSocket
utilities
state
- useBoolean
- useControllableState
- useCookie
- useCookies
- useCounter
- useCycleList
- useDefault
- useDisclosure
- useField
- useHash
- useList
- useLocalStorage
- useMap
- useMask
- useMergedRef
- useObject
- useOffsetPagination
- useQueue
- useRafState
- useRefState
- useSessionStorage
- useSet
- useStateHistory
- useStep
- useStorage
- useToggle
- useUrlSearchParam
- useUrlSearchParams
- useValidatedState
- useWizard
user
sensors
- useDeviceMotion
- useDeviceOrientation
- useHotkeys
- useIdle
- useInfiniteScroll
- useIntersectionObserver
- useKeyboard
- useKeyPress
- useKeysPressed
- useMouse
- useMutationObserver
- useOrientation
- usePageLeave
- useParallax
- usePerformanceObserver
- useResizeObserver
- useScroll
- useScrollIntoView
- useScrollTo
- useSwipe
- useTextSelection
- useVisibility
- useWindowEvent
- useWindowFocus
- useWindowScroll
- useWindowSize
Hey siberiacancode 👋07:11 AM
SC
Hey! What’s up?07:11 AM
Just shipped useAutoScroll today07:11 AM
SC
Nice! Does it pause on manual scroll?07:11 AM
Yep, it just works ✨07:11 AM
SC
Let me try it out07:11 AM
import type { SubmitEvent } from 'react';
import { useAutoScroll, useEventListener, useField, useInterval } from '@siberiacancode/reactuse';
import { ArrowDownIcon, SendIcon } from 'lucide-react';
import { useState } from 'react';
import { cn } from '@/utils/lib';
interface Message {
author: 'reactuse' | 'siberiacancode';
avatar?: string;
id: number;
text: string;
time: string;
}
const POKEMON_IDS = [1, 4, 7];
const random = <T,>(items: T[]): T => items[Math.floor(Math.random() * items.length)];
const getPokemonAvatar = (id: number) =>
`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${id}.png`;
const getRandomPokemonAvatar = () => getPokemonAvatar(random(POKEMON_IDS));
const REACTUSE_REPLIES = [
'Hey! How is the new release going?',
'I just shipped a new hook 🚀',
'Check out useAutoScroll — it just works.',
'You can scroll up and pause auto-scroll, btw.',
'No dependencies, no config needed.',
'Coffee break? ☕',
'Did you see the new docs?',
'TypeScript types are fully covered.',
'Working on a fresh demo right now.',
'Star us on GitHub if you like it ⭐',
'Got any feedback on the API?',
'How do you like the new docs theme?',
'Have you tried useDisclosure yet?',
'I love how composable React hooks are',
'BRB, grabbing some snacks 🍪',
'Anyone else excited for the next release?'
];
const formatTime = () => new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const INITIAL_MESSAGES: Message[] = [
{
id: 1,
author: 'reactuse',
text: 'Hey siberiacancode 👋',
time: formatTime(),
avatar: getRandomPokemonAvatar()
},
{ id: 2, author: 'siberiacancode', text: 'Hey! What’s up?', time: formatTime() },
{
id: 3,
author: 'reactuse',
text: 'Just shipped useAutoScroll today',
time: formatTime(),
avatar: getRandomPokemonAvatar()
},
{
id: 4,
author: 'siberiacancode',
text: 'Nice! Does it pause on manual scroll?',
time: formatTime()
},
{
id: 5,
author: 'reactuse',
text: 'Yep, it just works ✨',
time: formatTime(),
avatar: getRandomPokemonAvatar()
},
{ id: 6, author: 'siberiacancode', text: 'Let me try it out', time: formatTime() }
];
const SCROLL_THRESHOLD = 20;
const MAX_MESSAGES = 20;
const Demo = () => {
const [messages, setMessages] = useState<Message[]>(INITIAL_MESSAGES);
const messageField = useField('');
const [showNewMessage, setShowNewMessage] = useState(false);
const autoScrollRef = useAutoScroll<HTMLDivElement>();
useEventListener(autoScrollRef, 'scroll', () => {
const container = autoScrollRef.current;
if (!container) return;
const isAtBottom =
container.scrollHeight - container.scrollTop - container.clientHeight < SCROLL_THRESHOLD;
if (isAtBottom) setShowNewMessage(false);
});
useInterval(() => {
const container = autoScrollRef.current;
const isAtBottom = container
? container.scrollHeight - container.scrollTop - container.clientHeight < SCROLL_THRESHOLD
: true;
setMessages((currentMessages) =>
[
...currentMessages,
{
id: Math.random(),
author: 'reactuse' as const,
text: random(REACTUSE_REPLIES),
time: formatTime(),
avatar: getRandomPokemonAvatar()
}
].slice(-MAX_MESSAGES)
);
if (!isAtBottom) setShowNewMessage(true);
}, 4000);
const onScrollToBottom = () => {
const container = autoScrollRef.current;
if (!container) return;
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
setShowNewMessage(false);
};
const message = messageField.watch();
const onSubmit = (event: SubmitEvent<HTMLFormElement>) => {
event.preventDefault();
const trimmed = message.trim();
if (!trimmed) return;
setMessages((prev) =>
[
...prev,
{
id: Math.random(),
author: 'siberiacancode' as const,
text: trimmed,
time: formatTime()
}
].slice(-MAX_MESSAGES)
);
messageField.setValue('');
setShowNewMessage(false);
};
return (
<section className='flex w-md min-w-xs flex-col items-center'>
<div className='flex w-full flex-col gap-3 rounded-2xl border px-4 pb-4'>
<div className='relative'>
<div
ref={autoScrollRef}
className='no-scrollbar flex h-80 flex-col gap-3 overflow-y-auto scroll-smooth'
>
{messages.map((message) => {
const isMe = message.author === 'siberiacancode';
return (
<div
key={message.id}
className={cn('flex items-end gap-2', isMe && 'flex-row-reverse')}
>
{isMe && (
<div
className='size-7 shrink-0 bg-gradient-to-br from-neutral-700 to-neutral-900 text-[10px] font-semibold text-white'
data-slot='avatar'
>
<span data-slot='avatar-fallback'>SC</span>
</div>
)}
{!isMe && (
<div
className='size-7 shrink-0 bg-neutral-200 dark:bg-neutral-800'
data-slot='avatar'
>
<img
alt='reactuse'
className='translate-x-1 translate-y-1.5 scale-130 object-cover object-top'
data-slot='avatar-image'
src={message.avatar}
/>
</div>
)}
<div
className={cn(
'relative flex max-w-[75%] items-end gap-2 rounded-xl px-3 py-2 pr-12 text-sm',
isMe
? 'bg-primary text-primary-foreground rounded-br-sm'
: 'bg-muted rounded-bl-sm'
)}
>
<span>{message.text}</span>
<span
className={cn(
'absolute right-3 bottom-1 text-[9px] opacity-60',
isMe ? 'text-primary-foreground' : 'text-muted-foreground'
)}
>
{message.time}
</span>
</div>
</div>
);
})}
</div>
{showNewMessage && (
<button
className='absolute bottom-2 left-1/2 flex h-7 -translate-x-1/2 items-center gap-1.5 rounded-full px-3 text-xs shadow-md'
data-variant='outline'
type='button'
onClick={onScrollToBottom}
>
<ArrowDownIcon className='size-3' />
new message
</button>
)}
</div>
<form className='relative flex items-center gap-2' onSubmit={onSubmit}>
<input
className='h-11! rounded-full!'
placeholder='Type a message...'
{...messageField.register()}
/>
<button
className='absolute top-1/2 right-1 h-8! -translate-y-1/2 rounded-full! p-2!'
disabled={!message}
type='submit'
>
<SendIcon className='size-5' />
</button>
</form>
</div>
</section>
);
};
export default Demo;
Installation
pnpm add @siberiacancode/reactuseUsage
useAutoScroll(ref);
// or
const ref = useAutoScroll();Type Declarations
import type { HookTarget } from '@/utils/helpers';
import type { StateRef } from '../useRefState/useRefState';
export interface UseAutoScrollOptions {
/** Whether auto-scrolling is enabled */
enabled?: boolean;
/** Whether to force auto-scrolling regardless of user interactions */
force?: boolean;
}
export interface UseAutoScroll {
(target: HookTarget, options?: UseAutoScrollOptions): void;
<Target extends HTMLElement>(options?: UseAutoScrollOptions): StateRef<Target>;
}API
Parameters
| Name | Type | Default | Note |
|---|---|---|---|
| target | HookTarget | - | The target element to auto-scroll |
| options.enabled | boolean | - | Whether auto-scrolling is enabled |
Returns
voidParameters
| Name | Type | Default | Note |
|---|---|---|---|
| options.enabled | boolean | - | Whether auto-scrolling is enabled |
Returns
StateRef<Target>Contributors
ddebabin
Last updated on