# Domet
### Introduction
Domet is a lightweight React hook built for scroll-driven interfaces. Use it for classic scroll-spy, but also for progress indicators, lazy section loading, or any UI that needs reliable section awareness.
Lightweight under the hood: a tight scroll loop and hysteresis for stable, flicker-free section tracking.
For the source code, check out the [GitHub](https://github.com/blksmr/domet).
### Installation
Install the package from your command line.
```bash
npm install domet
```
### Usage
Basic example of how to use the hook.
```tsx showLineNumbers
import { useDomet } from 'domet'
const sections = ['intro', 'features', 'api']
function Page() {
const { activeId, sectionProps, navProps } = useDomet(sections)
return (
<>
.........
>
)
}
```
### API Reference
### Arguments
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `sectionIds` | `string[]` | — | Array of section IDs to track |
| `containerRef` | `RefObject \| null` | `null` | Scrollable container (defaults to window) |
| `options` | `DometOptions` | `{}` | Configuration options |
### Options
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `offset` | `number` | `0` | Trigger offset from top in pixels |
| `offsetRatio` | `number` | `0.08` | Viewport ratio for trigger line calculation |
| `debounceMs` | `number` | `10` | Throttle delay in milliseconds |
| `visibilityThreshold` | `number` | `0.6` | Minimum visibility ratio (0-1) for section to get priority |
| `hysteresisMargin` | `number` | `150` | Score margin to prevent rapid section switching |
| `behavior` | `'smooth' \| 'instant' \| 'auto'` | `'auto'` | Scroll behavior. 'auto' respects prefers-reduced-motion |
### Callbacks
| Prop | Type | Description |
|------|------|-------------|
| `onActiveChange` | `(id: string \| null, prevId: string \| null) => void` | Called when active section changes |
| `onSectionEnter` | `(id: string) => void` | Called when a section enters the viewport |
| `onSectionLeave` | `(id: string) => void` | Called when a section leaves the viewport |
| `onScrollStart` | `() => void` | Called when scrolling starts |
| `onScrollEnd` | `() => void` | Called when scrolling stops |
### Return Value
| Prop | Type | Description |
|------|------|-------------|
| `activeId` | `string \| null` | ID of the currently active section |
| `activeIndex` | `number` | Index of the active section in sectionIds (-1 if none) |
| `scroll` | `ScrollState` | Global scroll state |
| `sections` | `Record` | Per-section state indexed by ID |
| `sectionProps` | `(id: string) => SectionProps` | Props to spread on section elements |
| `navProps` | `(id: string) => NavProps` | Props to spread on nav items |
| `registerRef` | `(id: string) => (el: HTMLElement \| null) => void` | Manual ref registration |
| `scrollToSection` | `(id: string) => void` | Programmatically scroll to a section |
### Types
### ScrollState
Global scroll information updated on every scroll event.
```ts
type ScrollState = {
y: number // Current scroll position in pixels
progress: number // Overall scroll progress (0-1)
direction: 'up' | 'down' | null // Scroll direction
velocity: number // Scroll speed
isScrolling: boolean // True while actively scrolling
maxScroll: number // Maximum scroll value
viewportHeight: number // Viewport height in pixels
offset: number // Effective trigger offset
}
```
### SectionState
Per-section state available for each tracked section.
```ts
type SectionState = {
bounds: SectionBounds // Position and dimensions
visibility: number // Visibility ratio (0-1)
progress: number // Section scroll progress (0-1)
isInViewport: boolean // True if any part is visible
isActive: boolean // True if this is the active section
}
type SectionBounds = {
top: number
bottom: number
height: number
}
```
### Examples
### With Callbacks
```tsx
const { activeId } = useDomet(sections, null, {
onActiveChange: (id, prevId) => {
console.log(`Changed from ${prevId} to ${id}`)
},
onSectionEnter: (id) => {
console.log(`Entered: ${id}`)
},
})
```
### Using Scroll State
```tsx
const { scroll, sections } = useDomet(sectionIds)
// Global progress bar
// Per-section animations
{sectionIds.map(id => (
))}
```
### Custom Container
```tsx
const containerRef = useRef(null)
const { activeId } = useDomet(sections, containerRef)
return (
{/* sections */}
)
```
### Fine-tuning Behavior
```tsx
useDomet(sections, null, {
visibilityThreshold: 0.8, // Require 80% visibility
hysteresisMargin: 200, // More resistance to switching
})
```
### Why domet?
This library was born from a real need at work. I wanted a scroll-spy solution that was powerful and completely headless, but above all, extremely lightweight. No bloated dependencies, no opinionated styling, just a hook that does one thing well.
The name **domet** comes from Bosnian/Serbian/Croatian and means "reach" or "range" — the distance something can cover. Pronounced `/ˈdɔ.met/`: stress on the first syllable, open "o", and a hard "t" at the end.
### Support
For issues or feature requests, open an issue on [GitHub](https://github.com/blksmr/domet).
For LLMs, the full documentation is available at [/llms.txt](/llms.txt).
You can also reach out to me on [Twitter](https://x.com/blkasmir).