Skip to content
uiify

Pagination

Links or buttons for moving between pages of results.

Browsers
Chrome 123+Firefox 121+Safari 17.5+
Tier
0
Bundle compressed
2.59 kB
Forced layout budget
0 ms
Axe budget
0
SSR compatible
Yes
Published contract
Test harness

Playground

Pagination

import { Pagination } from "@gunkin/uiify";

export default function Example() {
  return (
    <Pagination.Root className="" count={20} defaultPage={5}>
      <Pagination.List />
    </Pagination.Root>
  );
}
@layer components {
  [data-uiify-pagination] ol {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 0.25rem;
    list-style: none;
    padding: 0;
    margin: 0;
  }
  [data-uiify-pagination] li {
    display: grid;
    place-items: center;
  }

  /* Page links and the previous/next controls are Button-skinned (`ghost`); the current
     page is the selected one. Links stay links, so only the pointer is restored here. */
  [data-uiify-pagination] [data-type="ellipsis"] {
    display: inline-grid;
    place-items: center;
    min-inline-size: 2.75rem;
    color: var(--muted-foreground);
  }
  [data-uiify-pagination] span[data-uiify-button]:not([data-disabled]) {
    cursor: default;
  }
  [data-uiify-pagination] svg {
    inline-size: 1rem;
    block-size: 1rem;
  }
}
import { Pagination } from "@gunkin/uiify";

export default function Example() {
  return (
    <Pagination.Root
      className="
        [&_ol]:flex [&_ol]:flex-wrap [&_ol]:items-center [&_ol]:justify-center [&_ol]:[gap:0.25rem] [&_ol]:list-none
        [&_ol]:p-0 [&_ol]:m-0 [&_li]:grid [&_li]:place-items-center [&_[data-type=ellipsis]]:inline-grid
        [&_[data-type=ellipsis]]:place-items-center [&_[data-type=ellipsis]]:[min-inline-size:2.75rem]
        [&_[data-type=ellipsis]]:text-muted-foreground [&_span[data-uiify-button]:not([data-disabled])]:cursor-default
        [&_svg]:[inline-size:1rem] [&_svg]:[block-size:1rem]
      "
      count={20}
      defaultPage={5}
    >
      <Pagination.List />
    </Pagination.Root>
  );
}
/* app.css */

@layer reset, ui, app, tokens, base, demo, components, utilities, theme;

@import "@gunkin/uiify/styles/reset";

@import "@gunkin/uiify/components/behavior.css";

@import "@gunkin/uiify/styles/tokens";

@import "@gunkin/uiify/styles/tailwind";

@import "tailwindcss/utilities.css";

@theme { --spacing: 0.25rem; --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; }

@source inline('[&_ol]:flex [&_ol]:flex-wrap [&_ol]:items-center [&_ol]:justify-center [&_ol]:[gap:0.25rem] [&_ol]:list-none [&_ol]:p-0 [&_ol]:m-0 [&_li]:grid [&_li]:place-items-center [&_[data-type=ellipsis]]:inline-grid [&_[data-type=ellipsis]]:place-items-center [&_[data-type=ellipsis]]:[min-inline-size:2.75rem] [&_[data-type=ellipsis]]:text-muted-foreground [&_span[data-uiify-button]:not([data-disabled])]:cursor-default [&_svg]:[inline-size:1rem] [&_svg]:[block-size:1rem]');

@layer base { :root { --uiify-control-text: var(--text-sm); } @media (pointer: coarse) { :root { --uiify-control-text: var(--text-base); } } }

/* Authored animation and registered properties used by the Tailwind skin.
 * Visual rules belong in utility recipes; only at-rule definitions live here. */
@layer components {
  @keyframes uiify-presence-in {
    from {
      opacity: 0;
      scale: var(--presence-scale);
    }
  }
}

@layer components {
  @keyframes uiify-presence-out {
    to {
      opacity: 0;
      scale: var(--presence-scale);
    }
  }
}

@property --uiify-spinner-tail {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@property --uiify-spinner-length {
  syntax: "<angle>";
  inherits: false;
  initial-value: 36deg;
}

@layer components {
  @keyframes uiify-spinner-rotate {
    to {
      transform: rotate(360deg);
    }
  }
}

@layer components {
  @keyframes uiify-spinner-orbit {
    0% {
      --uiify-spinner-tail: 0deg;
      --uiify-spinner-length: 36deg;
    }

    50% {
      --uiify-spinner-tail: 90deg;
      --uiify-spinner-length: 324deg;
    }

    100% {
      --uiify-spinner-tail: 360deg;
      --uiify-spinner-length: 36deg;
    }
  }
}

@layer components {
  @keyframes uiify-skeleton-pulse {
    50% {
      opacity: 0.45;
    }
  }
}

@layer components {
  @keyframes uiify-skeleton-shimmer {
    to {
      background-position: -200% 0;
    }
  }
}

@layer components {
  @keyframes uiify-skeleton-reveal {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }
}

@property --candle-body-height {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 0%;
}

@property --candle-body-top {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --candle-slot {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 100%;
}

@property --candle-wick-height {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 0%;
}

@property --candle-wick-top {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --candle-x {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 0%;
}

@property --candle-scale {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

@property --line-y {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --line-prev-y {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --line-count {
  syntax: "<integer>";
  inherits: false;
  initial-value: 1;
}

@property --line-scale {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

@layer components {
  @keyframes uiify-otp-input-caret-blink {
    50% {
      opacity: 0;
    }
  }
}

API

Pagination.Root

PropTypeDefaultDescription
aria-labelstring"Pagination"Accessible name of the navigation landmark.
boundaryCountnumber1Number of pages always shown at each end of the range.
count*number—Total number of pages.
defaultPagenumber1Page selected initially when the component is left uncontrolled.
disabledbooleanfalseDisables every page and navigation control.
onPageChange(page: number) => void—Called with the new page number whenever the page changes.
pagenumber—Current page, making the component controlled.
showFirstLastbooleanfalseAdds first and last page controls to the generated items.
siblingCountnumber1Number of pages shown on each side of the current page.
childrenReactNode—Custom item markup; when omitted the generated list of pages is rendered instead.
getPageHref(page: number) => string—Builds the href for each page so the server shell renders real links that work without JavaScript.
renderReactElement | ((props, state) => ReactElement)—Replace the rendered element while keeping behavior and accessibility wiring.

Pagination.List

PropTypeDefaultDescription
childrenReactNode—Pagination items rendered inside the list.
renderReactElement | ((props, state) => ReactElement)—Replace the rendered element while keeping behavior and accessibility wiring.

Pagination.Item

PropTypeDefaultDescription
childrenReactNode—Content of one pagination list item.
renderReactElement | ((props, state) => ReactElement)—Replace the rendered element while keeping behavior and accessibility wiring.

Pagination.Link

Pagination.Ellipsis

PropTypeDefaultDescription
childrenReactNode—Ellipsis content, hidden from assistive technology.
renderReactElement | ((props, state) => ReactElement)—Replace the rendered element while keeping behavior and accessibility wiring.

Keyboard

KeyAction
TabMove focus to/from the control
Enter / SpaceActivate when focused (button-like controls)

Accessibility

Uses semantic HTML where possible. Inspect rendered markup in the playground — ARIA is added only when native elements are insufficient.

Styling hooks

data-disabled