Pagination
Overview
The Pagination is a navigation component used for moving between pages of a result set such as tables, search results, and content listings. It is composed of three recipe parts: usePaginationRecipe() for the container that controls layout direction and spacing, usePaginationItemRecipe() for individual page-number buttons with color, variant, and interactive state options, and usePaginationEllipsisRecipe() for the non-interactive … element that collapses long page ranges. Each composable creates a fully configured recipe with compound variants that handle the color-variant combinations automatically.
The Pagination recipes integrate directly with the default design tokens preset and generate type-safe utility classes at build time with zero runtime CSS.
Why use the Pagination recipe?
The Pagination recipe helps you:
- Ship faster with sensible defaults: Get 2 orientations, 3 colors, 6 visual styles, and 3 sizes out of the box with three composable calls.
- Compose flexible navigation: Three coordinated recipes (container, item, ellipsis) share the size axis, so your pagination stays internally consistent.
- Maintain consistency: Compound variants ensure every color-variant combination follows the same design rules, including hover, focus, active, and dark mode states.
- Customize without forking: Override base styles, default variants, or filter out options you don't need — all through the options API.
- Stay type-safe: Full TypeScript support means your editor catches invalid color, variant, or size values at compile time.
- Integrate with your tokens: Every value references the design tokens preset, so theme changes propagate automatically.
Usage
Register the recipes
Add the Pagination recipes to a local Styleframe instance. The global styleframe.config.ts provides design tokens and utilities, while the component-level file registers the recipes themselves:
import { styleframe } from 'virtual:styleframe';
import {
usePaginationRecipe,
usePaginationItemRecipe,
usePaginationEllipsisRecipe,
} from '@styleframe/theme';
const s = styleframe();
const pagination = usePaginationRecipe(s);
const paginationItem = usePaginationItemRecipe(s);
const paginationEllipsis = usePaginationEllipsisRecipe(s);
export default s;
Build the component
Import the pagination, paginationItem, and paginationEllipsis runtime functions from the virtual module and pass variant props to compute class names:
import { pagination, paginationItem, paginationEllipsis } from "virtual:styleframe";
interface PaginationProps {
orientation?: "horizontal" | "vertical";
size?: "sm" | "md" | "lg";
children?: React.ReactNode;
}
interface PaginationItemProps {
color?: "light" | "dark" | "neutral";
variant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link";
size?: "sm" | "md" | "lg";
active?: boolean;
disabled?: boolean;
href?: string;
"aria-label"?: string;
children?: React.ReactNode;
}
export function Pagination({
orientation = "horizontal",
size = "md",
children,
}: PaginationProps) {
return (
<nav
className={pagination({ orientation, size })}
role="navigation"
aria-label="Pagination"
>
{children}
</nav>
);
}
export function PaginationItem({
color = "neutral",
variant = "ghost",
size = "md",
active = false,
disabled = false,
href,
children,
...rest
}: PaginationItemProps) {
const classes = paginationItem({
color,
variant,
size,
active: active ? "true" : "false",
disabled: disabled ? "true" : "false",
});
const Tag = href ? "a" : "button";
return (
<Tag
className={classes}
href={href && !disabled ? href : undefined}
disabled={!href && disabled ? true : undefined}
aria-current={active ? "page" : undefined}
aria-disabled={disabled || undefined}
{...rest}
>
{children}
</Tag>
);
}
export function PaginationEllipsis({ size = "md" }: { size?: "sm" | "md" | "lg" }) {
return (
<span className={paginationEllipsis({ size })} aria-hidden="true">
…
</span>
);
}
<script setup lang="ts">
import { pagination } from "virtual:styleframe";
const {
orientation = "horizontal",
size = "md",
} = defineProps<{
orientation?: "horizontal" | "vertical";
size?: "sm" | "md" | "lg";
}>();
</script>
<template>
<nav
:class="pagination({ orientation, size })"
role="navigation"
aria-label="Pagination"
>
<slot />
</nav>
</template>
<script setup lang="ts">
import { paginationItem } from "virtual:styleframe";
const {
color = "neutral",
variant = "ghost",
size = "md",
active = false,
disabled = false,
href,
} = defineProps<{
color?: "light" | "dark" | "neutral";
variant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link";
size?: "sm" | "md" | "lg";
active?: boolean;
disabled?: boolean;
href?: string;
}>();
</script>
<template>
<a
v-if="href !== undefined"
:class="paginationItem({ color, variant, size, active: active ? 'true' : 'false', disabled: disabled ? 'true' : 'false' })"
:href="disabled ? undefined : href"
:aria-current="active ? 'page' : undefined"
:aria-disabled="disabled || undefined"
>
<slot />
</a>
<button
v-else
type="button"
:class="paginationItem({ color, variant, size, active: active ? 'true' : 'false', disabled: disabled ? 'true' : 'false' })"
:disabled="disabled"
:aria-current="active ? 'page' : undefined"
>
<slot />
</button>
</template>
<script setup lang="ts">
import { paginationEllipsis } from "virtual:styleframe";
const { size = "md" } = defineProps<{
size?: "sm" | "md" | "lg";
}>();
</script>
<template>
<span :class="paginationEllipsis({ size })" aria-hidden="true">…</span>
</template>
See it in action
Orientation
The Pagination container recipe supports two orientations that control the flex layout direction. Horizontal arranges items in a row (the default and most common pagination layout), while vertical stacks items in a column for sidebar-style page lists.
Colors
The Pagination Item recipe includes 3 color variants: light, dark, and neutral. These match the Container color pattern used by Card, Modal, and Tooltip — surface-spectrum colors designed for navigation chrome rather than status communication. Each color is combined with every visual style variant through compound variants, so you get consistent, predictable styling across all combinations — including hover, focus, active, and dark mode states.
The neutral color adapts automatically: it uses a light appearance in light mode and a dark appearance in dark mode, making it the safest default for general-purpose pagination.
Color Reference
| Color | Token | Use Case |
|---|---|---|
light | @color.white / @color.gray-* | Light surfaces, stays light in dark mode |
dark | @color.gray-900 | Dark surfaces, stays dark in light mode |
neutral | Adaptive (light ↔ dark) | Default color, adapts to the current color scheme |
neutral as your default pagination color. It adapts automatically to the user's color scheme, so you don't need to manage light and dark variants separately.Variants
Six visual style variants control how the page-number items are rendered. Each variant is combined with the selected color through compound variants, so you always get the correct background, text, and border colors for your chosen color.
Solid
Filled background with contrasting text. The most prominent style, ideal when pagination is the primary navigation control on the page.
Outline
Transparent background with colored border and text. Useful when the pagination should remain visible without dominating the layout.
Soft
Light tinted background with no visible border. A subtle but visible style that works well for grouped pagination in dense table headers.
Subtle
Light tinted background with a matching border. Combines the softness of soft with added visual definition from a border.
Ghost
Transparent background that reveals a tinted background on hover. The default variant — ideal when pagination should appear as plain text until interaction.
Link
Styled as inline text links with no background or border. On hover, the text darkens and gains an underline. Use when pagination should look like ordinary navigation links.
Sizes
Three size variants from sm to lg control the font size, padding, and gap of the items, the ellipsis, and the container. Pass the same size value to all three sub-recipes so heights stay aligned.
| Size | Item Font Size | Item Padding (V / H) | Container Gap |
|---|---|---|---|
sm | @font-size.sm | @0.375 / @0.625 | @0.25 |
md | @font-size.sm | @0.5 / @0.75 | @0.375 |
lg | @font-size.md | @0.625 / @0.875 | @0.5 |
size prop must be passed to each sub-recipe individually. The container controls the gap and overall scale, while each item and ellipsis manages its own padding and font size.Anatomy
The Pagination recipe is composed of three independent recipes that work together to form a navigation row:
| Part | Recipe | Role |
|---|---|---|
| Container | usePaginationRecipe() | Outer <nav> flex container that controls orientation and gap |
| Item | usePaginationItemRecipe() | Individual page-number button (also used for prev/next/first/last with icon content) |
| Ellipsis | usePaginationEllipsisRecipe() | Non-interactive <span> that renders … between page ranges |
Each part is a standalone recipe with its own set of variants. The color and variant props belong to the item recipe, since the container is purely structural and the ellipsis is always neutral. Pass the size prop consistently to the container and every item / ellipsis so heights line up.
<!-- All three parts working together -->
<nav class="pagination(...)" role="navigation" aria-label="Pagination">
<button class="paginationItem(...)" aria-label="Previous page">‹</button>
<a class="paginationItem(...)" href="?page=1">1</a>
<a class="paginationItem(...) paginationItem--active-true" href="?page=2" aria-current="page">2</a>
<a class="paginationItem(...)" href="?page=3">3</a>
<span class="paginationEllipsis(...)" aria-hidden="true">…</span>
<a class="paginationItem(...)" href="?page=9">9</a>
<button class="paginationItem(...)" aria-label="Next page">›</button>
</nav>
<PaginationItem> instances with an icon child (‹, ›, «, ») and an aria-label. The item recipe handles their styling automatically, including the disabled state for "previous on page 1" and "next on the last page".Accessibility
- Use a
<nav>landmark. The container should render as<nav>withrole="navigation"(implicit on<nav>) and anaria-label="Pagination"so assistive technologies announce it as a distinct navigation region. - Mark the current page. Add
aria-current="page"to the active item. Screen readers announce "current page" alongside the page number. - Label the prev/next/first/last controls. These items typically render with icon content (
‹,›,«,»). Add an explicitaria-label="Previous page","Next page","First page", or"Last page"so the control has an accessible name. - Hide the ellipsis from assistive tech. The ellipsis is purely visual. Set
aria-hidden="true"so screen readers skip it instead of announcing "horizontal ellipsis". - Disable, don't hide. When the user is on the first page, mark the previous control with
disabled(button) oraria-disabled="true"(anchor) instead of removing it. Layout stability prevents the page numbers from shifting. - Keyboard navigation. Items render as
<button>or<a>, which are natively focusable and activatable withEnter/Space. The:focus-visiblering in the recipe base styles ensures the current focus is always visible.
Customization
Overriding Defaults
Each pagination composable accepts an optional second argument to override any part of the recipe configuration. Overrides are deep-merged with the defaults, so you only need to specify the properties you want to change:
import { styleframe } from 'virtual:styleframe';
import {
usePaginationRecipe,
usePaginationItemRecipe,
usePaginationEllipsisRecipe,
} from '@styleframe/theme';
const s = styleframe();
const pagination = usePaginationRecipe(s, {
defaultVariants: {
orientation: 'horizontal',
size: 'lg',
},
});
const paginationItem = usePaginationItemRecipe(s, {
base: { borderRadius: '@border-radius.full' },
defaultVariants: {
color: 'neutral',
variant: 'soft',
size: 'lg',
active: 'false',
disabled: 'false',
},
});
const paginationEllipsis = usePaginationEllipsisRecipe(s, {
defaultVariants: { size: 'lg' },
});
export default s;
Filtering Variants
If you only need a subset of the available variants, use the filter option to limit which values are generated. This reduces the output CSS and keeps your component API focused:
import { styleframe } from 'virtual:styleframe';
import { usePaginationItemRecipe } from '@styleframe/theme';
const s = styleframe();
// Only generate neutral color with ghost and solid styles
const paginationItem = usePaginationItemRecipe(s, {
filter: {
color: ['neutral'],
variant: ['ghost', 'solid'],
},
});
export default s;
API Reference
usePaginationRecipe(s, options?)
Creates the pagination container recipe with <nav>-friendly base styles, an orientation axis, and a size axis.
Parameters:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom base styles for the pagination container |
options.variants | Variants | Custom variant definitions for the recipe |
options.defaultVariants | Record<keyof Variants, string> | Default variant values for the recipe |
options.compoundVariants | CompoundVariant[] | Custom compound variant definitions for the recipe |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
orientation | horizontal, vertical | horizontal |
size | sm, md, lg | md |
usePaginationItemRecipe(s, options?)
Creates the pagination item recipe for individual page-number buttons. Renders consistently as a <button> or <a> and supports the full Interactive variant set plus boolean active and disabled axes.
Parameters: identical to usePaginationRecipe.
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | neutral |
variant | solid, outline, soft, subtle, ghost, link | ghost |
size | sm, md, lg | md |
active | true, false | false |
disabled | true, false | false |
usePaginationEllipsisRecipe(s, options?)
Creates the pagination ellipsis recipe for the non-interactive … element. Always renders with the @color.text-weak token; only the size axis is exposed.
Parameters: identical to usePaginationRecipe.
Variants:
| Variant | Options | Default |
|---|---|---|
size | sm, md, lg | md |
Best Practices
- Always wrap pagination in a
<nav>: The container renders as a navigation landmark witharia-label="Pagination"so assistive tech can jump straight to it. - Mark the current page with
aria-current="page": Set it on the item withactive=trueso screen readers announce the current position in the page list. - Pass
sizeto every sub-recipe: The container, items, and ellipsis each manage their own dimensions. Skipping one breaks vertical alignment. - Style the active page distinctly: The default
activeaxis only bumps font-weight to semibold. For a stronger highlight, passvariant="solid"to the active item while siblings stay onghost— see the active-vs-inactive contrast example. - Disable, don't hide, the edge controls: When the user is on the first or last page, set
disabledon the corresponding prev/next/first/last item rather than removing it. Layout stability beats minor space savings. - Filter what you don't need: Pass a
filteroption (e.g.,color: ['neutral']) to reduce generated CSS in projects that only use one color. - Override defaults at the recipe level: If your pagination is always large and outlined, set those as
defaultVariantsso component consumers write less code.
FAQ
size axis, keeping each recipe focused and letting you compose only the parts you need.<PaginationItem> instances with an icon child (‹, ›, «, ») and an explicit aria-label. The item recipe handles their styling and disabled state automatically. The WithControls story demonstrates the full layout with both edge and step controls.light, dark, and neutral to provide surface variations that work across all content types without implying a specific status.active axis bumps the item's font-weight to semibold — a subtle highlight that stays consistent with the rest of the row. For a stronger visual treatment, pass a different variant to the active item (for example, solid while siblings stay on ghost). The two approaches compose: you can set both active=true and a different variant on the same item.size prop to <PaginationEllipsis> as to your <PaginationItem> instances. The ellipsis recipe sets display: inline-flex with align-items: center and matches the items' vertical padding, so passing the same size keeps everything on the same baseline.The Pagination Item recipe uses compound variants to map each color-variant combination to specific styles. For example, when color is neutral and variant is ghost, the compound variant applies color: @color.text plus hover, focus, and active backgrounds, with dark mode overrides that switch text color and backgrounds. This keeps the individual color and variant definitions clean while handling all 18 combinations (3 colors × 6 variants) automatically.
filter option, compound variants that reference filtered-out values are automatically removed. For example, if you filter variant to only ['ghost', 'solid'], all compound variants matching outline, soft, subtle, or link are excluded from the generated output. Default variants are also adjusted if they reference a removed value.PageHero
A multi-part landing-section primitive composed of a section container plus headline, title, description, actions, links, image, and backdrop sub-recipes. Supports light, dark, and neutral colors with size, orientation, and alignment axes.
Overview
Explore Styleframe's comprehensive design token system. Create consistent, scalable design systems with composable functions for colors, typography, spacing, and more.