Skip to content

Instantly share code, notes, and snippets.

@vidyesh95
Created January 6, 2026 11:25
Show Gist options
  • Select an option

  • Save vidyesh95/7a7e690cfef5af5e9e4fa52299321931 to your computer and use it in GitHub Desktop.

Select an option

Save vidyesh95/7a7e690cfef5af5e9e4fa52299321931 to your computer and use it in GitHub Desktop.
dynamic pagination
<script lang="ts">
/**
* WatchlistPagination Component
*
* A smart pagination component that handles large page counts by showing
* a condensed view with ellipses.
*
* Core Behavior:
* - Always shows the first page (1) and the last page (totalPages).
* - Near the start (pages 1-3): Shows first 5 pages, then ellipsis, then last page.
* - Near the end (last 3 pages): Shows first page, ellipsis, then last 5 pages.
* - In the middle: Shows first page, ellipsis, 3-page window around current, ellipsis, last page.
* - Clicking an ellipsis (...) opens a Popover input for direct page jump.
*
* Pagination Pattern Examples (Total = 99):
* - Page 1: [1], 2, 3, 4, 5, ..., 99
* - Page 2: 1, [2], 3, 4, 5, ..., 99
* - Page 3: 1, 2, [3], 4, 5, ..., 99
* - Page 7: 1, ..., 6, [7], 8, ..., 99
* - Page 32: 1, ..., 31, [32], 33, ..., 99
* - Page 97: 1, ..., 95, 96, [97], 98, 99
* - Page 98: 1, ..., 95, 96, 97, [98], 99
* - Page 99: 1, ..., 95, 96, 97, 98, [99]
*/
import { Popover } from '@pratikbhadane24/nimbus-ui';
/**
* Props for the WatchlistPagination component.
*/
interface Props {
/** Total number of pages/watchlists available */
totalPages?: number;
/** Currently active page (1-indexed). Can be bound to the parent state. */
activePage?: number;
/** Callback triggered when the page is changed via click or direct input */
onPageChange?: (page: number) => void;
}
let { totalPages = 13, activePage = $bindable(1), onPageChange }: Props = $props();
// State for ellipsis popover UI
let leftPopoverOpen = $state(false);
let rightPopoverOpen = $state(false);
let popupInputValue = $state('');
/**
* Updates the active page and triggers the change callback.
* @param page The page number to navigate to.
*/
function handlePageClick(page: number) {
activePage = page;
onPageChange?.(page);
}
/**
* Handles keyboard input in the ellipsis jump popover.
* Navigates on 'Enter' if the input is valid.
* @param e Keyboard event.
*/
function handlePopupSubmit(e: KeyboardEvent) {
if (e.key === 'Enter') {
const page = parseInt(popupInputValue, 10);
if (!isNaN(page) && page >= 1 && page <= totalPages) {
handlePageClick(page);
}
closePopovers();
} else if (e.key === 'Escape') {
closePopovers();
}
}
/**
* Closes any open ellipsis jump popovers and resets the input value.
*/
function closePopovers() {
leftPopoverOpen = false;
rightPopoverOpen = false;
popupInputValue = '';
}
/**
* Computes the array of page numbers and ellipsis markers to display.
*
* Implementation Logic:
* 1. If totalPages <= 7, show all pages without ellipses.
* 2. If the activePage is near the start (1-3), show pages 1-5, ellipsis, lastPage.
* 3. If the activePage is near the end (lastPage-2 to lastPage), show 1, ellipsis, lastPage-4 to lastPage.
* 4. Otherwise (middle), show 1, ellipsis, activePage-1 to activePage+1, ellipsis, lastPage.
*
* @returns Array of page numbers or ellipsis identifiers.
*/
function getVisiblePages(): (number | 'left-ellipsis' | 'right-ellipsis')[] {
// Show all pages if total is small enough
if (totalPages <= 7) {
return Array.from({ length: totalPages }, (_, i) => i + 1);
}
const pages: (number | 'left-ellipsis' | 'right-ellipsis')[] = [];
// Case 1: Near the start (pages 1, 2, 3) - show first 5 pages
if (activePage <= 3) {
for (let i = 1; i <= 5; i++) {
pages.push(i);
}
pages.push('right-ellipsis');
pages.push(totalPages);
}
// Case 2: Near the end (last 3 pages) - show last 5 pages
else if (activePage >= totalPages - 2) {
pages.push(1);
pages.push('left-ellipsis');
for (let i = totalPages - 4; i <= totalPages; i++) {
pages.push(i);
}
}
// Case 3: Middle - show 3-page window around current
else {
pages.push(1);
pages.push('left-ellipsis');
pages.push(activePage - 1);
pages.push(activePage);
pages.push(activePage + 1);
pages.push('right-ellipsis');
pages.push(totalPages);
}
return pages;
}
let visiblePages = $derived(getVisiblePages());
</script>
<div class="flex items-center gap-2 overflow-x-auto py-2 px-1 scrollbar-hide">
{#each visiblePages as item}
{#if item === 'left-ellipsis'}
<Popover.Root bind:open={leftPopoverOpen}>
<Popover.Trigger
class="shrink-0 w-7 h-7 cursor-pointer flex items-center justify-center text-sm font-medium rounded-sm transition-all text-text-muted hover:bg-primary/10 hover:text-text"
>
...
</Popover.Trigger>
<Popover.Content
class="w-fit p-2 bg-card-background! border border-border rounded-md shadow-lg"
side="bottom"
align="center"
sideOffset={4}
>
<input
type="number"
min="1"
max={totalPages}
placeholder="Page"
bind:value={popupInputValue}
onkeydown={handlePopupSubmit}
class="w-16 px-2 py-1 text-sm text-text bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary"
/>
</Popover.Content>
</Popover.Root>
{:else if item === 'right-ellipsis'}
<Popover.Root bind:open={rightPopoverOpen}>
<Popover.Trigger
class="shrink-0 w-7 h-7 cursor-pointer flex items-center justify-center text-sm font-medium rounded-sm transition-all text-text-muted hover:bg-primary/10 hover:text-text"
>
...
</Popover.Trigger>
<Popover.Content
class="w-fit p-2 bg-card-background! border border-border rounded-md shadow-lg"
side="bottom"
align="center"
sideOffset={4}
>
<input
type="number"
min="1"
max={totalPages}
placeholder="Page"
bind:value={popupInputValue}
onkeydown={handlePopupSubmit}
class="w-16 px-2 py-1 text-sm text-text bg-background border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary"
/>
</Popover.Content>
</Popover.Root>
{:else}
<button
type="button"
onclick={() => handlePageClick(item)}
class="shrink-0 w-7 h-7 cursor-pointer flex items-center justify-center text-sm font-medium rounded-sm transition-all {activePage ===
item
? 'bg-primary/10 text-primary shadow-sm'
: 'text-text-muted bg-fixed-state hover:bg-primary/10 hover:text-text'}"
>
{item}
</button>
{/if}
{/each}
</div>
<style>
/* Hide scrollbar but allow scrolling */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Remove spinner from number input */
input[type='number']::-webkit-outer-spin-button,
input[type='number']::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type='number'] {
-moz-appearance: textfield;
appearance: textfield;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment