SectionProgress
A floating pill over a long read — a ring filling with scroll progress, the section being read, and a list to jump between them.
Installation
SectionProgress ships with the library — install it once, then import:
import { SectionProgress } from "@delacour/react-native-ui/section-progress";Or copy the source into your project, to own and edit it:
bunx delacour@latest add section-progressUsage
import { useScrollSections } from "@/hooks/use-scroll-sections";
import { SectionProgress } from "@/components/ui/section-progress";
import { Text } from "@/components/ui/text";
import { View } from "react-native";
import Animated from "react-native-reanimated";
const SECTIONS = [
{ id: "intro", title: "Introduction", body: "What this policy covers, and who it applies to." },
{ id: "pricing", title: "Pricing", body: "What each plan costs, and when you are billed." },
{ id: "faq", title: "Questions", body: "The answers people ask for most often." },
];
export default function Policy() {
const sections = useScrollSections({ readingLine: 96 });
return (
<View className="flex-1">
<Animated.ScrollView
ref={sections.scrollRef}
onScroll={sections.scrollHandler}
scrollEventThrottle={16}
contentContainerClassName="pb-32"
>
{SECTIONS.map((section) => (
<View key={section.id} onLayout={sections.register(section.id)}>
<Text.Header>{section.title}</Text.Header>
<Text.Paragraph>{section.body}</Text.Paragraph>
</View>
))}
</Animated.ScrollView>
<SectionProgress scroll={sections} value={sections.activeId} onValueChange={sections.scrollTo}>
{SECTIONS.map((section) => (
<SectionProgress.Item key={section.id} value={section.id}>
{section.title}
</SectionProgress.Item>
))}
</SectionProgress>
</View>
);
}useScrollSections tracks the scroll view; the pill reads its progress and scrollY through
scroll, its activeId as value and its scrollTo as onValueChange. The ring is drawn with Skia
and fills on the UI thread, so scrolling renders nothing — the label changes only when the section does.
Then the pill eases to the new title's width while the old title slides out and the new one slides in:
up for a later section, down for an earlier one.
Mount the pill as the last child of the screen's container, a sibling of the scroll view. It positions itself absolutely inside that container and is not portalled. Leave bottom padding at the end of the scroll content so the pill never covers the last line.
Each registered section must be a direct child of the scroll content: onLayout reports y
relative to its parent.
Placements
Six placements, offset points in from the safe area. A top-* pill opens its panel downward and a
bottom-* one upward; start and end mirror under right-to-left.
<SectionProgress placement="top-end" offset={12} …>…</SectionProgress>Reveal
The pill appears once the reader scrolls past revealAt (64 points by default) and stays from then
on, so it never blinks near the top. revealAt={0} shows it at once.
Coloured sections
An item's color paints its marker in the panel, and the ring while that section is being read.
<SectionProgress.Item value="known-issues" color="warning">Known issues</SectionProgress.Item>Manual progress
progress overrides the scroll — a number or a shared value from 0 to 1. Small steps track
directly; a jump of more than a tenth eases over 180ms.
<SectionProgress progress={step / steps.length} revealAt={0} value={current}>…</SectionProgress>Controlled open
Tapping the pill grows it into the card — one surface, its width, height and corners easing together
while the list fades in — and the card hugs its sections up to six, past which the list scrolls.
isOpen and onOpenChange hand the panel to you. The pill, an outside tap, the escape gesture and
Android back all close it through the same callback.
const [isOpen, setOpen] = useState(false);
<SectionProgress isOpen={isOpen} onOpenChange={setOpen} …>…</SectionProgress>Accessibility
- The pill is one button, read as "Sections, Pricing, 42 percent", with an expanded state.
- The percent only reaches React while a screen reader is running; with none, scrolling renders nothing.
- Opening moves focus to the active section. The panel is modal to VoiceOver, and the escape gesture closes it.
- Sections are buttons; the active one is announced selected. The ring and the markers are hidden.
- Android back closes the open panel.
- Reduce motion snaps the pill's and the card's size, and turns the list's reveal and the title's slide into fades. The ring keeps tracking — it is information, not decoration.
API Reference
Anatomy
| Part | What it is |
|---|---|
SectionProgress | The floating pill, its panel and the outside-tap scrim |
SectionProgress.Item | One section — a coloured marker and its title, which is also the pill's label while active |
SectionProgress
Extends ViewProps.
Prop
Type
SectionProgress.Item
A Pressable, so feedback, haptic and the rest are inherited — see
Pressable.
Prop
Type
Exports
import {
SectionProgress,
type SectionProgressProps,
type SectionProgressItemProps,
SECTION_PROGRESS_PLACEMENTS,
SECTION_PROGRESS_COLORS,
useSectionProgress,
} from "@delacour/react-native-ui/section-progress";