import { PageControl, PageControlItem } from '@vega-ui/react';
<PageControl aria-label='Onboarding pages'>
<PageControlItem index={0} aria-label='Welcome' />
<PageControlItem index={1} aria-label='Connect your calendar' />
<PageControlItem index={2} aria-label='Invite your team' />
<PageControlItem index={3} aria-label='Start working' />
</PageControl>- Browser support
- Newly available 2024
- Chrome 125
- Edge 125
- Firefox 128
- Safari 18
- Safari on iOS 18
- Bundle size
Summary
PageControl is a compound component for short page or step indicators, usually in carousels and onboarding flows. It supports discrete items and animated progress items.
Imports
import {
PageControl,
PageControlItem,
PageControlProgress,
type PageControlItemProps,
type PageControlProgressProps,
type PageControlProps,
} from '@vega-ui/react';Exported Types
PageControlPropsPageControlItemPropsPageControlProgressProps
Minimal Composition
PageControl renders a tablist and every item a tab, but neither has a visible label: pass aria-label to the root and to each item. Use defaultActive for an uncontrolled control, or active with onChangeActive when the slides live in parent state.
import { PageControl, PageControlItem } from '@vega-ui/react';
export default function MinimalComposition() {
return (
<PageControl defaultActive={0} aria-label='Slides'>
<PageControlItem index={0} aria-label='Slide 1 of 3' />
<PageControlItem index={1} aria-label='Slide 2 of 3' />
<PageControlItem index={2} aria-label='Slide 3 of 3' />
</PageControl>
);
}Required Parts
PageControl: root active-state coordinatorPageControlItem: discrete dot item
Optional Parts
PageControlProgress: animated progress-style item
Composition Order
PageControl- repeated
PageControlItemorPageControlProgress
Variants
- Size:
sm,md,lg - State model: controlled or uncontrolled
active - Item style: static dot or progress indicator
Edge Cases
- This pattern works best for short sequences.
- Progress items introduce timing semantics that should match real autoplay or step-advance behavior.
- Active state should not rely on color alone.
Common Mistakes
- Using
PageControlfor long result navigation. - Forgetting to keep active state in sync with the actual carousel or onboarding step.
- Rendering too many items for the available space.
Examples
Basic
Basic: carousel indicators synced with the slide
The active index lives in state and drives both the dots and the visible slide. Each dot has an id, an aria-label with its position and aria-controls pointing at the slide panel, which is a tabpanel labelled by the active dot.
import { Card, Heading, PageControl, PageControlItem, Text } from '@vega-ui/react';
import { useId, useState } from 'react';
const slides = [
{ title: 'Plan your week', text: 'Drag tasks onto the calendar and see your free time at a glance.' },
{ title: 'Share with your team', text: 'Invite teammates to a board and assign tasks in one click.' },
{ title: 'Stay on track', text: 'Get a summary every Monday with what slipped and what is next.' },
];
export default function CarouselIndicators() {
const prefix = useId();
const [active, setActive] = useState(0);Controlled/Stateful
Controlled/Stateful: external previous and next buttons
The same active value is changed from three places: the dots, the arrow keys inside the dots and the icon buttons on both sides. The caption above shows the current photo and its position, and the buttons are disabled at the ends.
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';
import { Icon, IconButton, PageControl, PageControlItem, Text } from '@vega-ui/react';
import { useState } from 'react';
const photos = ['Lisbon tram', 'Sintra palace', 'Cascais beach', 'Porto bridge', 'Douro valley'];
export default function ControlledActiveIndex() {
const [active, setActive] = useState(0);
const isFirst = active === 0;
const isLast = active === photos.length - 1;
Form/Integration
Form/Integration: step dots above a multi-step form
One <form> with a hidden <fieldset> per step. Continue validates only the current step with reportValidity, steps not reached yet are disabled, and onChangeActive ignores them as well, so the arrow keys cannot skip ahead. The final submit reads every step with FormData.
import { Button, HelperText, Label, PageControl, PageControlItem, Text, TextField, TextFieldInput } from '@vega-ui/react';
import { FormEvent, MouseEvent, useId, useState } from 'react';
const steps = ['Account', 'Workspace', 'Invite'];
export default function StepIndicatorAboveFormPages() {
const id = useId();
const [step, setStep] = useState(0);
const [reached, setReached] = useState(0);
const [result, setResult] = useState('');
const goTo = (index: number) => {Layout/Overlay
Layout/Overlay: “What’s new” dialog
Dots and a Next button share the footer of a dialog; the last step turns Next into a closing button. Reopening the dialog starts from the first update again.
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogTrigger,
Heading,
Icon,
IconButton,
PageControl,
PageControlItem,
Text,Edge
Edge: timed autoplay with a pause
PageControlProgress animates the active dot for duration milliseconds; its animationend event advances to the next story. Autoplay must be stoppable, so Pause swaps the timed items for plain PageControlItem dots, which stops the timer, and the title becomes a polite live region only while paused.
import { Button, PageControl, PageControlItem, PageControlProgress, Text } from '@vega-ui/react';
import { useState } from 'react';
const stories = ['New arrivals', 'Autumn sale', 'Gift cards', 'Store opening'];
export default function TimedProgressIndicators() {
const [active, setActive] = useState(0);
const [paused, setPaused] = useState(false);
const advance = () => setActive((active + 1) % stories.length);
return (Edge: thirty pages in a window of seven dots
Thirty dots would be noise, so only a window of seven around the active item is rendered, each keeping its real index. The window recenters as you move, so the arrow keys still reach every photo, and the caption gives the real position.
import { PageControl, PageControlItem, Text } from '@vega-ui/react';
import { useState } from 'react';
const total = 30;
const windowSize = 7;
export default function ManyPagesWindow() {
const [active, setActive] = useState(0);
const half = Math.floor(windowSize / 2);
const first = Math.min(Math.max(active - half, 0), total - windowSize);
const visible = Array.from({ length: windowSize }, (_, offset) => first + offset);Anatomy
Overview
PageControl coordinates a short sequence of tab-like indicators. The root owns active index and pointer/keyboard interaction, while child items reflect the current state.
Required Parts
PageControl
Required. Owns active index and interaction logic.
PageControlItem
Required for normal dot-style indicators.
Optional Parts
PageControlProgress
Optional progress-style item with animated fill and duration.
Composition Order
PageControl- repeated
PageControlItemorPageControlProgress
Valid Composition Patterns
import { Card, Heading, PageControl, PageControlItem, Text } from '@vega-ui/react';
import { useId, useState } from 'react';
const slides = [
{ title: 'Plan your week', text: 'Drag tasks onto the calendar and see your free time at a glance.' },
{ title: 'Share with your team', text: 'Invite teammates to a board and assign tasks in one click.' },
{ title: 'Stay on track', text: 'Get a summary every Monday with what slipped and what is next.' },
];
export default function CarouselIndicators() {
const prefix = useId();
const [active, setActive] = useState(0);Invalid Composition Patterns
Items with duplicated indexes
Active state and roving focus become ambiguous.
Progress items without matching step-advance logic
The animation implies timed advancement that the parent flow may not actually perform.
Very long sequences
The component loses clarity when used like full pagination.