Skip to content

Commit 2811677

Browse files
feat!: CSS custom properties + custom link component (#82)
Swap Sass for plain CSS modules, expose 18 theme values as CSS custom properties on .next-pagination, and add a linkComponent prop so consumers can inject a custom/wrapped next/link. Refs #82 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2e49f87 commit 2811677

8 files changed

Lines changed: 291 additions & 260 deletions

File tree

README.md

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,55 @@ You'll need to load the actual data from your API yourself. We're only here for
5959

6060
| Name | Type | Description |
6161
| ------------------------ | ---------- | ----------------------------------------- |
62-
| `total` | `Number` | **Required.** The total number of pages. |
63-
| `theme` | `Object` | A CSS modules style object. |
64-
| `sizes` | `Array` | An array of page size numbers |
65-
| `perPageText` | `String` | Label for the page size dropdown |
66-
| `setPageSizeText` | `String` | Label for the invisible page size button |
67-
| `linkProps` | `Object` | Extra props to pass to the next.js links |
62+
| `total` | `Number` | **Required.** The total number of pages. |
63+
| `theme` | `Object` | A CSS modules style object. |
64+
| `sizes` | `Array` | An array of page size numbers |
65+
| `perPageText` | `String` | Label for the page size dropdown |
66+
| `setPageSizeText` | `String` | Label for the invisible page size button |
67+
| `linkProps` | `Object` | Extra props to pass to the link component |
68+
| `linkComponent` | `Component` | Custom link component. Defaults to `next/link`. Receives `href` + a legacy-behaviour anchor child. |
6869

6970
## Theming
70-
Next.js natively supports **CSS modules**, so this component supports injecting CSS module styles.
7171

72-
Import the styles as you would for a normal component, but pass them as props.
72+
### CSS custom properties (recommended)
73+
74+
The component exposes every theme value as a CSS custom property scoped to `.next-pagination`. Override any of them in your own stylesheet:
75+
76+
```css
77+
.my-container .next-pagination {
78+
--next-pagination-interactive-color: hotpink;
79+
--next-pagination-border-radius: 8px;
80+
}
81+
```
82+
83+
Available variables:
84+
85+
```
86+
--next-pagination-interactive-color
87+
--next-pagination-spacing-vertical
88+
--next-pagination-spacing-horizontal
89+
--next-pagination-spacing-vertical-sm
90+
--next-pagination-spacing-horizontal-sm
91+
--next-pagination-border-width
92+
--next-pagination-border-radius
93+
--next-pagination-line-height
94+
--next-pagination-item-background
95+
--next-pagination-item-background-current
96+
--next-pagination-item-background-disabled
97+
--next-pagination-item-color
98+
--next-pagination-item-color-current
99+
--next-pagination-item-color-disabled
100+
--next-pagination-item-border-color
101+
--next-pagination-select-background
102+
--next-pagination-select-border-color
103+
--next-pagination-select-border-color-hover
104+
```
105+
106+
### Full class-name override
107+
108+
For deeper customisation, pass a CSS modules style object via the `theme` prop:
73109

74110
```jsx
75-
[...]
76111
import styles from '/my/path/to/styles.module.css'
77112

78113
class Example extends Component {
@@ -82,7 +117,23 @@ class Example extends Component {
82117
}
83118
```
84119

85-
The theme uses BEM class naming with the base class `next-pagination`. The file `/src/index.module.scss` should give you a solid idea of what's needed.
120+
The theme uses BEM class naming with the base class `next-pagination`. The file `/src/index.module.css` shows the full class list.
121+
122+
## Custom link component
123+
124+
By default the component renders navigation links with `next/link`. If you need to wrap or replace it (e.g. to add a custom prefetch strategy, swap in an app-router variant, or inject a shared analytics wrapper), pass your own via `linkComponent`:
125+
126+
```jsx
127+
import NextLink from 'next/link'
128+
129+
const TrackedLink = (props) => (
130+
<NextLink {...props} onClick={() => track('pagination')} />
131+
)
132+
133+
<Pagination total={1000} linkComponent={TrackedLink} />
134+
```
135+
136+
The component must accept `href`, `prefetch`, `passHref`, `legacyBehavior`, and a single anchor child.
86137

87138
## Contribute
88139

example/package-lock.json

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

Lines changed: 0 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
"react": "^18.2.0",
5454
"react-dom": "^18.2.0",
5555
"react-scripts": "^5.0.1",
56-
"sass": "1.26.5",
5756
"typescript": "^4.6.4"
5857
},
5958
"files": [

src/declarations.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
declare module '*.scss'
1+
declare module '*.css'

src/index.module.css

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
.next-pagination {
2+
--next-pagination-interactive-color: #72256d;
3+
4+
--next-pagination-spacing-vertical: 1em;
5+
--next-pagination-spacing-horizontal: 1em;
6+
7+
--next-pagination-spacing-vertical-sm: calc(var(--next-pagination-spacing-vertical) / 2);
8+
--next-pagination-spacing-horizontal-sm: calc(var(--next-pagination-spacing-horizontal) / 2);
9+
10+
--next-pagination-border-width: 1px;
11+
--next-pagination-border-radius: 4px;
12+
--next-pagination-line-height: 24px;
13+
14+
--next-pagination-item-background: #fff;
15+
--next-pagination-item-background-current: #f7f8fa;
16+
--next-pagination-item-background-disabled: var(--next-pagination-item-background);
17+
18+
--next-pagination-item-color: var(--next-pagination-interactive-color);
19+
--next-pagination-item-color-current: #666;
20+
--next-pagination-item-color-disabled: var(--next-pagination-item-color-current);
21+
22+
--next-pagination-item-border-color: #edeef2;
23+
24+
--next-pagination-select-background: #fff;
25+
--next-pagination-select-border-color: var(--next-pagination-item-border-color);
26+
--next-pagination-select-border-color-hover: var(--next-pagination-interactive-color);
27+
28+
align-items: center;
29+
box-sizing: border-box;
30+
display: flex;
31+
flex-direction: row-reverse;
32+
justify-content: space-between;
33+
line-height: var(--next-pagination-line-height);
34+
user-select: none;
35+
}
36+
37+
.next-pagination * {
38+
box-sizing: inherit;
39+
}
40+
41+
.next-pagination__list {
42+
display: flex;
43+
list-style-type: none;
44+
margin: 0;
45+
padding: 0;
46+
}
47+
48+
.next-pagination__item {
49+
border: var(--next-pagination-border-width) solid var(--next-pagination-item-border-color);
50+
border-left-color: transparent;
51+
display: none;
52+
margin-right: calc(var(--next-pagination-border-width) * -1);
53+
}
54+
55+
@media screen and (min-width: 37.5em) {
56+
.next-pagination__item {
57+
display: block;
58+
}
59+
}
60+
61+
.next-pagination__item:first-child,
62+
.next-pagination__item:last-child {
63+
display: block;
64+
}
65+
66+
.next-pagination__item:first-child {
67+
border-left-color: var(--next-pagination-item-border-color);
68+
border-radius: var(--next-pagination-border-radius) 0 0 var(--next-pagination-border-radius);
69+
}
70+
71+
.next-pagination__item:first-child .next-pagination__link {
72+
border-radius: var(--next-pagination-border-radius) 0 0 var(--next-pagination-border-radius);
73+
}
74+
75+
.next-pagination__item:last-child {
76+
border-radius: 0 var(--next-pagination-border-radius) var(--next-pagination-border-radius) 0;
77+
border-right-width: var(--next-pagination-border-width);
78+
margin-right: 0;
79+
}
80+
81+
.next-pagination__item:last-child .next-pagination__link {
82+
border-radius: 0 var(--next-pagination-border-radius) var(--next-pagination-border-radius) 0;
83+
}
84+
85+
.next-pagination__item--hellip {
86+
min-width: 2.5em;
87+
padding: var(--next-pagination-spacing-vertical-sm) var(--next-pagination-spacing-horizontal-sm);
88+
text-align: center;
89+
}
90+
91+
.next-pagination__link {
92+
background: var(--next-pagination-item-background);
93+
color: var(--next-pagination-item-color);
94+
display: block;
95+
min-width: 2.5em;
96+
outline: var(--next-pagination-border-width) solid transparent;
97+
padding: var(--next-pagination-spacing-vertical-sm) var(--next-pagination-spacing-horizontal-sm);
98+
text-align: center;
99+
text-decoration: none;
100+
transition: outline-color .2s ease-in-out;
101+
}
102+
103+
.next-pagination__link:hover,
104+
.next-pagination__link:focus {
105+
outline: var(--next-pagination-border-width) solid currentColor;
106+
position: relative;
107+
z-index: 1;
108+
}
109+
110+
.next-pagination__link--disabled {
111+
background: var(--next-pagination-item-background-disabled);
112+
color: var(--next-pagination-item-color-disabled);
113+
pointer-events: none;
114+
}
115+
116+
.next-pagination__link--current {
117+
background: var(--next-pagination-item-background-current);
118+
color: var(--next-pagination-item-color-current);
119+
pointer-events: none;
120+
}
121+
122+
.next-pagination__link svg {
123+
display: block;
124+
}
125+
126+
.next-pagination__form {
127+
align-items: center;
128+
display: flex;
129+
flex-direction: row-reverse;
130+
}
131+
132+
.next-pagination__label {
133+
flex: 0 0 auto;
134+
margin-left: var(--next-pagination-spacing-horizontal-sm);
135+
}
136+
137+
.next-pagination__select {
138+
background-color: var(--next-pagination-select-background);
139+
border: var(--next-pagination-border-width) solid var(--next-pagination-select-border-color);
140+
border-radius: var(--next-pagination-border-radius);
141+
color: inherit;
142+
display: block;
143+
font-size: 1em;
144+
line-height: var(--next-pagination-line-height);
145+
position: relative;
146+
text-overflow: ellipsis;
147+
transition: border-color .2s ease-in-out;
148+
width: 100%;
149+
}
150+
151+
.next-pagination__select:focus-within,
152+
.next-pagination__select:hover {
153+
border-color: var(--next-pagination-select-border-color-hover);
154+
outline: none;
155+
}
156+
157+
.next-pagination__select select {
158+
appearance: none;
159+
background: transparent;
160+
border: 0 none;
161+
display: block;
162+
height: calc((var(--next-pagination-spacing-vertical-sm) * 2) + var(--next-pagination-line-height) + (var(--next-pagination-border-width) * 2));
163+
font-size: 1em;
164+
line-height: var(--next-pagination-line-height);
165+
padding: var(--next-pagination-spacing-vertical-sm) var(--next-pagination-spacing-horizontal-sm);
166+
padding-right: 2.5em;
167+
text-indent: var(--next-pagination-spacing-horizontal-sm);
168+
text-overflow: ellipsis;
169+
transition: border-color .2s ease-in-out;
170+
width: 100%;
171+
z-index: 1;
172+
}
173+
174+
.next-pagination__select select:focus {
175+
outline: none;
176+
}
177+
178+
.next-pagination__select select::-ms-expand {
179+
display: none;
180+
}
181+
182+
.next-pagination__select-suffix {
183+
position: absolute;
184+
right: 0;
185+
top: 0;
186+
height: 100%;
187+
pointer-events: none;
188+
width: 2em;
189+
}
190+
191+
.next-pagination__select-suffix svg {
192+
display: block;
193+
height: 16px;
194+
left: 50%;
195+
position: relative;
196+
top: 50%;
197+
transform: translate(-50%, -50%);
198+
width: 16px;
199+
}
200+
201+
/* SR only */
202+
.next-pagination__submit {
203+
border: 0;
204+
clip: rect(0 0 0 0);
205+
clip: rect(0, 0, 0, 0);
206+
height: 1px;
207+
margin: -1px;
208+
overflow: hidden;
209+
padding: 0;
210+
position: absolute;
211+
width: 1px;
212+
}

0 commit comments

Comments
 (0)