Pagination
Page navigation with the current page clearly marked and long ranges collapsed with an ellipsis.
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious,} from "@/components/ui/pagination";import { useState } from "react";
const TOTAL = 10;
// 1 ... 3 [4] 5 ... 10, same page list as the Vue and Svelte demos.function pageItems( page: number,): (number | "ellipsis-start" | "ellipsis-end")[] { if (page <= 3) return [1, 2, 3, 4, 5, "ellipsis-end", TOTAL]; if (page >= TOTAL - 2) return [ 1, "ellipsis-start", TOTAL - 4, TOTAL - 3, TOTAL - 2, TOTAL - 1, TOTAL, ]; return [1, "ellipsis-start", page - 1, page, page + 1, "ellipsis-end", TOTAL];}
export default function Demo() { const [page, setPage] = useState(4); const go = (next: number) => (event: React.MouseEvent) => { event.preventDefault(); setPage(Math.min(TOTAL, Math.max(1, next))); };
return ( <Pagination> <PaginationContent> <PaginationItem> <PaginationPrevious href="#" onClick={go(page - 1)} aria-disabled={page === 1} /> </PaginationItem> {pageItems(page).map((item) => typeof item === "number" ? ( <PaginationItem key={item}> <PaginationLink href="#" isActive={item === page} onClick={go(item)} > {item} </PaginationLink> </PaginationItem> ) : ( <PaginationItem key={item}> <PaginationEllipsis /> </PaginationItem> ), )} <PaginationItem> <PaginationNext href="#" onClick={go(page + 1)} aria-disabled={page === TOTAL} /> </PaginationItem> </PaginationContent> </Pagination> );}<script setup lang="ts">import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationNext, PaginationPrevious,} from "@/components/ui/pagination";</script>
<template> <Pagination v-slot="{ page }" :total="100" :items-per-page="10" :default-page="4" :sibling-count="1" show-edges> <PaginationContent v-slot="{ items }"> <PaginationPrevious /> <template v-for="(item, index) in items" :key="index"> <PaginationItem v-if="item.type === 'page'" :value="item.value" :is-active="item.value === page"> {{ item.value }} </PaginationItem> <PaginationEllipsis v-else :index="index" /> </template> <PaginationNext /> </PaginationContent> </Pagination></template><script lang="ts"> import * as Pagination from "$lib/components/ui/pagination";</script>
<Pagination.Root count={100} perPage={10} page={4}> {#snippet children({ pages, currentPage })} <Pagination.Content> <Pagination.Item> <Pagination.Previous /> </Pagination.Item> {#each pages as page (page.key)} {#if page.type === "ellipsis"} <Pagination.Item> <Pagination.Ellipsis /> </Pagination.Item> {:else} <Pagination.Item> <Pagination.Link {page} isActive={currentPage === page.value}> {page.value} </Pagination.Link> </Pagination.Item> {/if} {/each} <Pagination.Item> <Pagination.Next /> </Pagination.Item> </Pagination.Content> {/snippet}</Pagination.Root>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/paginationpnpm dlx shadcn@latest add @edmi-ui/paginationyarn dlx shadcn@latest add @edmi-ui/paginationbunx shadcn@latest add @edmi-ui/paginationPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
button,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/paginationpnpm dlx shadcn-vue@latest add @edmi-ui/paginationyarn dlx shadcn-vue@latest add @edmi-ui/paginationbunx shadcn-vue@latest add @edmi-ui/paginationPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install reka-ui @vueuse/coreTerminal window pnpm add reka-ui @vueuse/coreTerminal window yarn add reka-ui @vueuse/coreTerminal window bun add reka-ui @vueuse/coreAdd the Edmi components this one builds on:
button,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/pagination.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/pagination.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/pagination.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/pagination.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
button,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious,} from "@/components/ui/pagination";
<Pagination> <PaginationContent> <PaginationItem> <PaginationPrevious href="#" /> </PaginationItem> <PaginationItem> <PaginationNext href="#" /> </PaginationItem> </PaginationContent></Pagination><script setup lang="ts">import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationNext, PaginationPrevious,} from "@/components/ui/pagination";</script>
<template> <Pagination v-slot="{ page }" :total="100" :items-per-page="10" :default-page="4" :sibling-count="1" show-edges> <PaginationContent v-slot="{ items }"> <PaginationPrevious /> <template v-for="(item, index) in items" :key="index"> <PaginationItem v-if="item.type === 'page'" :value="item.value" :is-active="item.value === page"> {{ item.value }} </PaginationItem> <PaginationEllipsis v-else :index="index" /> </template> <PaginationNext /> </PaginationContent> </Pagination></template><script lang="ts"> import * as Pagination from "$lib/components/ui/pagination";</script>
<Pagination.Root count={100} perPage={10} page={4}> {#snippet children({ pages, currentPage })} <Pagination.Content> <Pagination.Item> <Pagination.Previous /> </Pagination.Item> {#each pages as page (page.key)} {#if page.type === "ellipsis"} <Pagination.Item> <Pagination.Ellipsis /> </Pagination.Item> {:else} <Pagination.Item> <Pagination.Link {page} isActive={currentPage === page.value}> {page.value} </Pagination.Link> </Pagination.Item> {/if} {/each} <Pagination.Item> <Pagination.Next /> </Pagination.Item> </Pagination.Content> {/snippet}</Pagination.Root>Component source
Section titled “Component source”"use client";
import { cn } from "cn";import * as React from "react";import { CaretLeftIcon, CaretRightIcon, DotsThreeIcon } from "@phosphor-icons/react";import { Button } from "@/registry/edmi/ui/button";import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";
// ✦ `elevation` on Pagination flows to the active PaginationLink (only the active link rises).const PaginationContext = React.createContext<{ raised: boolean }>({ raised: false,});
function Pagination({ className, elevation, ...props}: React.ComponentProps<"nav"> & { /** ✦ depth: raised +1 / floating +2 make the active page link rise. */ elevation?: Elevation;}) { const level = useElevation(elevation, "control"); return ( <PaginationContext.Provider value={{ raised: level === "raised" || level === "floating" }} > <nav aria-label="pagination" data-slot="pagination" className={cn("mx-auto flex w-full justify-center", className)} {...props} /> </PaginationContext.Provider> );}
function PaginationContent({ className, ...props}: React.ComponentProps<"ul">) { return ( <ul data-slot="pagination-content" className={cn("flex items-center gap-0.5", className)} {...props} /> );}
function PaginationItem({ ...props }: React.ComponentProps<"li">) { return <li data-slot="pagination-item" {...props} />;}
type PaginationLinkProps = { isActive?: boolean; /** ✦ depth of this link (the active link rises); defaults to the Pagination level. */ elevation?: Elevation;} & Pick<React.ComponentProps<typeof Button>, "size"> & React.ComponentProps<"a">;
function PaginationLink({ className, isActive, elevation, size = "icon", ...props}: PaginationLinkProps) { const context = React.useContext(PaginationContext); const own = elevation && elevation !== "auto" ? elevation === "raised" || elevation === "floating" : undefined; return ( <Button variant={isActive ? "outline" : "ghost"} size={size} className={cn( isActive && "border-input bg-card font-semibold", isActive && (own ?? context.raised) && "border-transparent shadow-btn-raised-neutral", className, )} nativeButton={false} render={ <a aria-current={isActive ? "page" : undefined} data-slot="pagination-link" data-active={isActive} {...props} /> } /> );}
function PaginationPrevious({ className, text = "Previous", ...props}: React.ComponentProps<typeof PaginationLink> & { text?: string }) { return ( <PaginationLink aria-label="Go to previous page" size="default" className={cn("pl-1.5!", className)} {...props} > <CaretLeftIcon data-icon="inline-start" className="rtl:rotate-180" /> <span className="hidden sm:block">{text}</span> </PaginationLink> );}
function PaginationNext({ className, text = "Next", ...props}: React.ComponentProps<typeof PaginationLink> & { text?: string }) { return ( <PaginationLink aria-label="Go to next page" size="default" className={cn("pr-1.5!", className)} {...props} > <span className="hidden sm:block">{text}</span> <CaretRightIcon data-icon="inline-end" className="rtl:rotate-180" /> </PaginationLink> );}
function PaginationEllipsis({ className, ...props}: React.ComponentProps<"span">) { return ( <span aria-hidden data-slot="pagination-ellipsis" className={cn( "flex size-8 items-center justify-center [&_svg:not([class*='size-'])]:size-4", className, )} {...props} > <DotsThreeIcon /> <span className="sr-only">More pages</span> </span> );}
export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious,};<script setup lang="ts">import type { PaginationRootEmits, PaginationRootProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { reactiveOmit } from "@vueuse/core"import { PaginationRoot, useForwardPropsEmits } from "reka-ui"import { computed, provide } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation"
const props = withDefaults(defineProps<PaginationRootProps & { class?: HTMLAttributes["class"] /** ✦ depth: raised +1 / floating +2 make the active page link rise */ elevation?: Elevation}>(), { elevation: undefined })
const level = useElevation(() => props.elevation, "control")const raised = computed(() => level.value === "raised" || level.value === "floating")
provide("pagination", { get raised() { return raised.value },})const emits = defineEmits<PaginationRootEmits>()
const delegatedProps = reactiveOmit(props, "class", "elevation")const forwarded = useForwardPropsEmits(delegatedProps, emits)</script>
<template> <PaginationRoot v-slot="slotProps" data-slot="pagination" v-bind="forwarded" :class="cn('mx-auto flex w-full justify-center', props.class)" > <slot v-bind="slotProps" /> </PaginationRoot></template><script setup lang="ts">import type { PaginationListProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { reactiveOmit } from "@vueuse/core"import { PaginationList } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<PaginationListProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")</script>
<template> <PaginationList v-slot="slotProps" data-slot="pagination-content" v-bind="delegatedProps" :class="cn('flex items-center gap-0.5', props.class)" > <slot v-bind="slotProps" /> </PaginationList></template><script setup lang="ts">import type { PaginationEllipsisProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { MoreHorizontal } from "@lucide/vue"import { reactiveOmit } from "@vueuse/core"import { PaginationEllipsis } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<PaginationEllipsisProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")</script>
<template> <PaginationEllipsis data-slot="pagination-ellipsis" v-bind="delegatedProps" :class="cn('flex size-8 items-center justify-center [&_svg:not([class*=size-])]:size-4', props.class)" > <slot> <MoreHorizontal /> <span class="sr-only">More pages</span> </slot> </PaginationEllipsis></template><script setup lang="ts">import type { PaginationFirstProps } from "reka-ui"import type { HTMLAttributes } from "vue"import type { ButtonVariants } from "@/registry/edmi/ui/button"import { ChevronLeftIcon } from "@lucide/vue"import { reactiveOmit } from "@vueuse/core"import { PaginationFirst, useForwardProps } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"import { buttonVariants } from "@/registry/edmi/ui/button"
const props = withDefaults(defineProps<PaginationFirstProps & { size?: ButtonVariants["size"] class?: HTMLAttributes["class"]}>(), { size: "default",})
const delegatedProps = reactiveOmit(props, "class", "size")const forwarded = useForwardProps(delegatedProps)</script>
<template> <PaginationFirst data-slot="pagination-first" :class="cn(buttonVariants({ variant: 'ghost', size }), 'pl-1.5!', props.class)" v-bind="forwarded" > <slot> <ChevronLeftIcon data-icon="inline-start" class="rtl:rotate-180" /> <span class="hidden sm:block">First</span> </slot> </PaginationFirst></template><script setup lang="ts">import type { PaginationListItemProps } from "reka-ui"import type { HTMLAttributes } from "vue"import type { ButtonVariants } from "@/registry/edmi/ui/button"import { reactiveOmit } from "@vueuse/core"import { PaginationListItem } from "reka-ui"import { computed, inject } from "vue"import { cn } from "@/registry/edmi/lib/utils"import type { Elevation } from "@/registry/edmi/ui/elevation"import { buttonVariants } from "@/registry/edmi/ui/button"
const props = withDefaults(defineProps<PaginationListItemProps & { size?: ButtonVariants["size"] class?: HTMLAttributes["class"] isActive?: boolean /** ✦ overrides the Pagination `elevation` for this link (the active link rises) */ elevation?: Elevation}>(), { size: "icon", elevation: undefined,})
const context = inject<{ raised?: boolean } | null>("pagination", null)
const delegatedProps = reactiveOmit(props, "class", "size", "isActive", "elevation")
const raised = computed(() => props.elevation && props.elevation !== "auto" ? props.elevation === "raised" || props.elevation === "floating" : !!context?.raised,)</script>
<template> <PaginationListItem data-slot="pagination-item" v-bind="delegatedProps" :class="cn( buttonVariants({ variant: isActive ? 'outline' : 'ghost', size, }), isActive && 'bg-card font-semibold', isActive && raised && 'border-transparent shadow-btn-raised-neutral', props.class)" > <slot /> </PaginationListItem></template><script setup lang="ts">import type { PaginationLastProps } from "reka-ui"import type { HTMLAttributes } from "vue"import type { ButtonVariants } from "@/registry/edmi/ui/button"import { ChevronRightIcon } from "@lucide/vue"import { reactiveOmit } from "@vueuse/core"import { PaginationLast, useForwardProps } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"import { buttonVariants } from "@/registry/edmi/ui/button"
const props = withDefaults(defineProps<PaginationLastProps & { size?: ButtonVariants["size"] class?: HTMLAttributes["class"]}>(), { size: "default",})
const delegatedProps = reactiveOmit(props, "class", "size")const forwarded = useForwardProps(delegatedProps)</script>
<template> <PaginationLast data-slot="pagination-last" :class="cn(buttonVariants({ variant: 'ghost', size }), 'pr-1.5!', props.class)" v-bind="forwarded" > <slot> <span class="hidden sm:block">Last</span> <ChevronRightIcon data-icon="inline-end" class="rtl:rotate-180" /> </slot> </PaginationLast></template><script setup lang="ts">import type { PaginationNextProps } from "reka-ui"import type { HTMLAttributes } from "vue"import type { ButtonVariants } from "@/registry/edmi/ui/button"import { ChevronRightIcon } from "@lucide/vue"import { reactiveOmit } from "@vueuse/core"import { PaginationNext, useForwardProps } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"import { buttonVariants } from "@/registry/edmi/ui/button"
const props = withDefaults(defineProps<PaginationNextProps & { size?: ButtonVariants["size"] class?: HTMLAttributes["class"]}>(), { size: "default",})
const delegatedProps = reactiveOmit(props, "class", "size")const forwarded = useForwardProps(delegatedProps)</script>
<template> <PaginationNext data-slot="pagination-next" :class="cn(buttonVariants({ variant: 'ghost', size }), 'pr-1.5!', props.class)" v-bind="forwarded" > <slot> <span class="hidden sm:block">Next</span> <ChevronRightIcon data-icon="inline-end" class="rtl:rotate-180" /> </slot> </PaginationNext></template><script setup lang="ts">import type { PaginationPrevProps } from "reka-ui"import type { HTMLAttributes } from "vue"import type { ButtonVariants } from "@/registry/edmi/ui/button"import { ChevronLeftIcon } from "@lucide/vue"import { reactiveOmit } from "@vueuse/core"import { PaginationPrev, useForwardProps } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"import { buttonVariants } from "@/registry/edmi/ui/button"
const props = withDefaults(defineProps<PaginationPrevProps & { size?: ButtonVariants["size"] class?: HTMLAttributes["class"]}>(), { size: "default",})
const delegatedProps = reactiveOmit(props, "class", "size")const forwarded = useForwardProps(delegatedProps)</script>
<template> <PaginationPrev data-slot="pagination-previous" :class="cn(buttonVariants({ variant: 'ghost', size }), 'pl-1.5!', props.class)" v-bind="forwarded" > <slot> <ChevronLeftIcon data-icon="inline-start" class="rtl:rotate-180" /> <span class="hidden sm:block">Previous</span> </slot> </PaginationPrev></template>export { default as Pagination } from "./Pagination.vue";export { default as PaginationContent } from "./PaginationContent.vue";export { default as PaginationEllipsis } from "./PaginationEllipsis.vue";export { default as PaginationFirst } from "./PaginationFirst.vue";export { default as PaginationItem } from "./PaginationItem.vue";export { default as PaginationLast } from "./PaginationLast.vue";export { default as PaginationNext } from "./PaginationNext.vue";export { default as PaginationPrevious } from "./PaginationPrevious.vue";<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLUListElement>> = $props();</script>
<ul bind:this={ref} data-slot="pagination-content" class={cn( "flex items-center gap-0.5", className)} {...restProps}> {@render children?.()}</ul><script lang="ts"> import DotsThreeIcon from "phosphor-svelte/lib/DotsThree"; import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, ...restProps }: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();</script>
<span bind:this={ref} aria-hidden="true" data-slot="pagination-ellipsis" class={cn( "flex size-8 items-center justify-center [&_svg:not([class*='size-'])]:size-4", className)} {...restProps}> <DotsThreeIcon /> <span class="sr-only">More pages</span></span><script lang="ts"> import type { WithElementRef } from "$lib/utils.js"; import type { HTMLLiAttributes } from "svelte/elements";
let { ref = $bindable(null), children, ...restProps }: WithElementRef<HTMLLiAttributes> = $props();</script>
<li bind:this={ref} data-slot="pagination-item" {...restProps}> {@render children?.()}</li><script lang="ts"> import { Pagination as PaginationPrimitive } from "bits-ui"; import { buttonVariants, type ButtonSize } from "$lib/registry/ui/button/index.js"; import { cn } from "$lib/utils.js"; import type { Elevation } from "$lib/registry/ui/elevation/index.js"; import { getPaginationCtx } from "./pagination.svelte"; let { ref = $bindable(null), class: className, size = "icon", isActive, page, elevation = "auto", children, ...restProps }: PaginationPrimitive.PageProps & { size?: ButtonSize; isActive: boolean; /** ✦ overrides the Pagination `elevation` (the active link rises). */ elevation?: Elevation; } = $props();
const ctx = getPaginationCtx(); const isRaised = $derived( elevation !== "auto" ? elevation === "raised" || elevation === "floating" : (ctx?.raised ?? false) );</script>
{#snippet Fallback()} {page.value}{/snippet}
<PaginationPrimitive.Page bind:ref {page} aria-current={isActive ? "page" : undefined} data-slot="pagination-link" data-active={isActive} data-size={size} class={cn( buttonVariants({ size, variant: isActive ? "outline" : "ghost" }), // paginationActive: flat outline, elevation ✦ raises it isActive && "bg-card font-semibold", isActive && isRaised && "border-transparent shadow-btn-raised-neutral", className )} {...restProps}> {#if children} {@render children?.()} {:else} {@render Fallback()} {/if}</PaginationPrimitive.Page><script lang="ts"> import { Pagination as PaginationPrimitive } from "bits-ui"; import CaretRightIcon from "phosphor-svelte/lib/CaretRight"; import { cn } from "$lib/utils.js"; import { buttonVariants } from "../button/index.js";
let { ref = $bindable(null), class: className, children, ...restProps }: PaginationPrimitive.NextButtonProps = $props();</script>
{#snippet Fallback()} <span>Next</span> <CaretRightIcon class={cn("rtl:rotate-180 size-4", className)} />{/snippet}
<PaginationPrimitive.NextButton bind:ref aria-label="Go to next page" class={cn(buttonVariants({ variant: "ghost" }), "pr-1.5!", className)} {...restProps}> {#if children} {@render children?.()} {:else} {@render Fallback()} {/if}</PaginationPrimitive.NextButton><script lang="ts"> import { Pagination as PaginationPrimitive } from "bits-ui"; import CaretRightIcon from "phosphor-svelte/lib/CaretRight"; import { buttonVariants } from "$lib/registry/ui/button/index.js"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, ...restProps }: PaginationPrimitive.NextButtonProps = $props();</script>
<PaginationPrimitive.NextButton bind:ref aria-label="Go to next page" class={cn(buttonVariants({ variant: "ghost", size: "default" }), "pr-1.5!", className)} {...restProps}> <span class="hidden sm:block">Next</span> <CaretRightIcon data-icon="inline-end" class="rtl:rotate-180" /></PaginationPrimitive.NextButton><script lang="ts"> import { Pagination as PaginationPrimitive } from "bits-ui"; import CaretLeftIcon from "phosphor-svelte/lib/CaretLeft"; import { cn } from "$lib/utils.js"; import { buttonVariants } from "$lib/registry/ui/button/index.js";
let { ref = $bindable(null), class: className, children, ...restProps }: PaginationPrimitive.PrevButtonProps = $props();</script>
{#snippet Fallback()} <CaretLeftIcon class={cn("rtl:rotate-180 size-4", className)} /> <span>Previous</span>{/snippet}
<PaginationPrimitive.PrevButton bind:ref aria-label="Go to previous page" class={cn(buttonVariants({ variant: "ghost" }), "pl-1.5!", className)} {...restProps}> {#if children} {@render children?.()} {:else} {@render Fallback()} {/if}</PaginationPrimitive.PrevButton><script lang="ts"> import { Pagination as PaginationPrimitive } from "bits-ui"; import CaretLeftIcon from "phosphor-svelte/lib/CaretLeft"; import { buttonVariants } from "$lib/registry/ui/button/index.js"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, ...restProps }: PaginationPrimitive.PrevButtonProps = $props();</script>
<PaginationPrimitive.PrevButton bind:ref aria-label="Go to previous page" class={cn( buttonVariants({ variant: "ghost", size: "default" }), "pl-1.5!", className )} {...restProps}> <CaretLeftIcon data-icon="inline-start" class="rtl:rotate-180" /> <span class="hidden sm:block">Previous</span></PaginationPrimitive.PrevButton><script lang="ts" module> import { getContext, setContext } from "svelte";
interface PaginationContext { raised: boolean; }
export function setPaginationCtx(ctx: PaginationContext) { setContext("pagination", ctx); }
export function getPaginationCtx() { return getContext<PaginationContext | undefined>("pagination"); }</script>
<script lang="ts"> import { Pagination as PaginationPrimitive } from "bits-ui"; import { cn } from "$lib/utils.js"; import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js";
let { ref = $bindable(null), class: className, count = 0, perPage = 10, page = $bindable(1), siblingCount = 1, elevation = "auto", ...restProps }: PaginationPrimitive.RootProps & { /** ✦ depth: raised +1 / floating +2 make the active link rise. */ elevation?: Elevation; } = $props();
const level = useElevation(() => elevation, "control"); const raised = $derived(level.current === "raised" || level.current === "floating");
setPaginationCtx({ get raised() { return raised; }, });</script>
<PaginationPrimitive.Root bind:ref bind:page role="navigation" aria-label="pagination" data-slot="pagination" {count} {perPage} {siblingCount} class={cn("mx-auto flex w-full justify-center", className)} {...restProps}/>import Root from "./pagination.svelte";import Content from "./pagination-content.svelte";import Ellipsis from "./pagination-ellipsis.svelte";import Item from "./pagination-item.svelte";import Link from "./pagination-link.svelte";import Next from "./pagination-next.svelte";import NextButton from "./pagination-next-button.svelte";import PrevButton from "./pagination-prev-button.svelte";import Previous from "./pagination-previous.svelte";
export { Content, Content as PaginationContent, Ellipsis, Ellipsis as PaginationEllipsis, Item, Item as PaginationItem, Link, Link as PaginationLink, Next, Next as PaginationNext, NextButton, // old NextButton as PaginationNextButton, // old PrevButton, // old PrevButton as PaginationPrevButton, // old Previous, Previous as PaginationPrevious, Root, // Root as Pagination,};Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
Flat by default. Set raised on Pagination: only the active link rises (bevel); the list never does. PaginationLink can override it.
Flat (0)
Raised (+1): only the active link rises
Flat (0)
Raised (+1): only the active link rises
Flat (0)
Raised (+1): only the active link rises
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious,} from "@/components/ui/pagination";import { useState } from "react";
const TOTAL = 10;
// 1 ... 3 [4] 5 ... 10, same page list as the Vue and Svelte demos.function pageItems( page: number,): (number | "ellipsis-start" | "ellipsis-end")[] { if (page <= 3) return [1, 2, 3, 4, 5, "ellipsis-end", TOTAL]; if (page >= TOTAL - 2) return [ 1, "ellipsis-start", TOTAL - 4, TOTAL - 3, TOTAL - 2, TOTAL - 1, TOTAL, ]; return [1, "ellipsis-start", page - 1, page, page + 1, "ellipsis-end", TOTAL];}
export default function Demo() { const [page, setPage] = useState(4); const go = (next: number) => (event: React.MouseEvent) => { event.preventDefault(); setPage(Math.min(TOTAL, Math.max(1, next))); };
return ( <div className="flex flex-col gap-5"> {(["flat", "raised"] as const).map((level) => ( <div key={level} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground"> {level === "flat" ? "Flat (0)" : "Raised (+1): only the active link rises"} </p> <Pagination elevation={level}> <PaginationContent> <PaginationItem> <PaginationPrevious href="#" onClick={go(page - 1)} aria-disabled={page === 1} /> </PaginationItem> {pageItems(page).map((item) => typeof item === "number" ? ( <PaginationItem key={item}> <PaginationLink href="#" isActive={item === page} onClick={go(item)} > {item} </PaginationLink> </PaginationItem> ) : ( <PaginationItem key={item}> <PaginationEllipsis /> </PaginationItem> ), )} <PaginationItem> <PaginationNext href="#" onClick={go(page + 1)} aria-disabled={page === TOTAL} /> </PaginationItem> </PaginationContent> </Pagination> </div> ))} </div> );}<script setup lang="ts">import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationNext, PaginationPrevious,} from "@/components/ui/pagination";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1): only the active link rises" }] as const;</script>
<template> <div class="flex flex-col gap-5"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <Pagination v-slot="{ page }" :elevation="level.value" :total="100" :items-per-page="10" :default-page="4" :sibling-count="1" show-edges> <PaginationContent v-slot="{ items }"> <PaginationPrevious /> <template v-for="(item, index) in items" :key="index"> <PaginationItem v-if="item.type === 'page'" :value="item.value" :is-active="item.value === page"> {{ item.value }} </PaginationItem> <PaginationEllipsis v-else :index="index" /> </template> <PaginationNext /> </PaginationContent> </Pagination> </div> </div></template><script lang="ts"> import * as Pagination from "$lib/components/ui/pagination";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1): only the active link rises" }] as const;</script>
<div class="flex flex-col gap-5"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <Pagination.Root count={100} perPage={10} page={4} elevation={level.value}> {#snippet children({ pages, currentPage })} <Pagination.Content> <Pagination.Item> <Pagination.Previous /> </Pagination.Item> {#each pages as page (page.key)} {#if page.type === "ellipsis"} <Pagination.Item> <Pagination.Ellipsis /> </Pagination.Item> {:else} <Pagination.Item> <Pagination.Link {page} isActive={currentPage === page.value}> {page.value} </Pagination.Link> </Pagination.Item> {/if} {/each} <Pagination.Item> <Pagination.Next /> </Pagination.Item> </Pagination.Content> {/snippet} </Pagination.Root> </div> {/each}</div>| Prop | Type | Default |
|---|---|---|
PaginationLink isActive |
boolean |
false |
PaginationLink size |
Button sizes | icon |
PaginationPrevious text / PaginationNext text |
string |
Previous / Next |
elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
Parts: Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationPrevious, PaginationNext, PaginationEllipsis.
✦ Edmi additions
Section titled “✦ Edmi additions”- Links are Edmi Buttons: active page is
outline, othersghost.