Skip to content
v2.5.0
English
Русский
Documentation

PageControl

PageControl is a compound component for short page or step indicators, usually in carousels and onboarding flows.
page-control.tsx · 8 lines
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
Tested in
23 tests

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

  • PageControlProps
  • PageControlItemProps
  • PageControlProgressProps

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.

minimal-composition.tsx · 11 lines
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 coordinator
  • PageControlItem: discrete dot item

Optional Parts

  • PageControlProgress: animated progress-style item

Composition Order

  1. PageControl
  2. repeated PageControlItem or PageControlProgress

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 PageControl for 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

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.

carousel-indicators.tsx · 39 lines
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.

controlled-active-index.tsx · 33 lines
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.

step-indicator-above-form-pages.tsx · 88 lines
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.

whats-new-dialog.tsx · 78 lines
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.

timed-progress-indicators.tsx · 37 lines
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.

many-pages-window.tsx · 29 lines
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

  1. PageControl
  2. repeated PageControlItem or PageControlProgress

Valid Composition Patterns

carousel-indicators.tsx · 39 lines
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.