PostCSS plugin to unwrap nested rules like how Sass does it
npx @tessl/cli install tessl/npm-postcss-nested@7.0.00
# PostCSS Nested
1
2
PostCSS Nested is a PostCSS plugin that unwraps nested CSS rules similar to Sass syntax. It enables developers to write nested CSS with parent selector references (&), automatic selector merging, at-rule bubbling, and custom root rule handling for breaking out of nesting contexts.
3
4
## Package Information
5
6
- **Package Name**: postcss-nested
7
- **Package Type**: npm
8
- **Language**: JavaScript
9
- **Installation**: `npm install postcss-nested`
10
11
## Core Imports
12
13
```javascript
14
const postcssNested = require('postcss-nested');
15
```
16
17
ESM:
18
19
```javascript
20
import postcssNested from 'postcss-nested';
21
```
22
23
## Basic Usage
24
25
```javascript
26
const postcss = require('postcss');
27
const postcssNested = require('postcss-nested');
28
29
// Basic usage with default options
30
const result = postcss([postcssNested()])
31
.process(css, { from: 'input.css' });
32
33
// With custom options
34
const result = postcss([
35
postcssNested({
36
bubble: ['phone'],
37
preserveEmpty: true,
38
rootRuleName: 'escape-nesting'
39
})
40
]).process(css, { from: 'input.css' });
41
```
42
43
Input CSS:
44
```css
45
.phone {
46
&_title {
47
width: 500px;
48
@media (max-width: 500px) {
49
width: auto;
50
}
51
body.is_dark & {
52
color: white;
53
}
54
}
55
img {
56
display: block;
57
}
58
}
59
60
.title {
61
font-size: var(--font);
62
63
@at-root html {
64
--font: 16px;
65
}
66
}
67
```
68
69
Output CSS:
70
```css
71
.phone_title {
72
width: 500px;
73
}
74
@media (max-width: 500px) {
75
.phone_title {
76
width: auto;
77
}
78
}
79
body.is_dark .phone_title {
80
color: white;
81
}
82
.phone img {
83
display: block;
84
}
85
86
.title {
87
font-size: var(--font);
88
}
89
html {
90
--font: 16px;
91
}
92
```
93
94
## Capabilities
95
96
### Plugin Factory Function
97
98
Creates a PostCSS plugin instance with optional configuration.
99
100
```javascript { .api }
101
/**
102
* Creates a PostCSS plugin for processing nested CSS rules
103
* @param {Options} opts - Optional configuration object
104
* @returns {PostCSSPlugin} PostCSS plugin object
105
*/
106
function postcssNested(opts = {}) {
107
// Returns PostCSS plugin
108
}
109
110
// PostCSS compatibility flag
111
postcssNested.postcss = true;
112
```
113
114
### Configuration Options
115
116
All configuration options are optional and have sensible defaults.
117
118
```javascript { .api }
119
interface Options {
120
/**
121
* Custom at-rules that should bubble to the top level.
122
* Default: ['media', 'supports', 'layer', 'container', 'starting-style']
123
*/
124
bubble?: string[];
125
126
/**
127
* Custom at-rules that should be unwrapped from nested contexts.
128
* Default: ['document', 'font-face', 'keyframes', '-webkit-keyframes', '-moz-keyframes']
129
*/
130
unwrap?: string[];
131
132
/**
133
* Whether to preserve empty selector rules after unwrapping.
134
* Useful for CSS modules compatibility.
135
* Default: false
136
*/
137
preserveEmpty?: boolean;
138
139
/**
140
* Custom name for the at-root directive for breaking out of nesting.
141
* Default: 'at-root'
142
*/
143
rootRuleName?: string;
144
}
145
```
146
147
### PostCSS Plugin Interface
148
149
The plugin returns a PostCSS plugin object with the required interface.
150
151
```javascript { .api }
152
interface PostCSSPlugin {
153
/** Plugin identifier for PostCSS */
154
postcssPlugin: 'postcss-nested';
155
156
/** Initial processing of at-root rules */
157
Once(root: Root): void;
158
159
/** Main rule processing function */
160
Rule(rule: Rule): void;
161
162
/** Final cleanup of at-root rules */
163
RootExit(root: Root): void;
164
}
165
```
166
167
## Key Features
168
169
### Nested Rules
170
Write CSS rules inside other rules, similar to Sass:
171
172
```css
173
/* Input */
174
.card {
175
padding: 10px;
176
177
.title {
178
font-size: 18px;
179
}
180
181
.content {
182
margin-top: 10px;
183
}
184
}
185
186
/* Output */
187
.card {
188
padding: 10px;
189
}
190
191
.card .title {
192
font-size: 18px;
193
}
194
195
.card .content {
196
margin-top: 10px;
197
}
198
```
199
200
### Parent Selector References
201
Use `&` to reference the parent selector:
202
203
```css
204
/* Input */
205
.button {
206
color: blue;
207
208
&:hover {
209
color: red;
210
}
211
212
&.active {
213
font-weight: bold;
214
}
215
216
.dark-theme & {
217
color: white;
218
}
219
}
220
221
/* Output */
222
.button {
223
color: blue;
224
}
225
226
.button:hover {
227
color: red;
228
}
229
230
.button.active {
231
font-weight: bold;
232
}
233
234
.dark-theme .button {
235
color: white;
236
}
237
```
238
239
### At-Rule Bubbling
240
Media queries and other at-rules bubble to the root level:
241
242
```css
243
/* Input */
244
.sidebar {
245
width: 300px;
246
247
@media (max-width: 768px) {
248
width: 100%;
249
250
.menu {
251
display: none;
252
}
253
}
254
}
255
256
/* Output */
257
.sidebar {
258
width: 300px;
259
}
260
261
@media (max-width: 768px) {
262
.sidebar {
263
width: 100%;
264
}
265
266
.sidebar .menu {
267
display: none;
268
}
269
}
270
```
271
272
### At-Rule Unwrapping
273
Certain at-rules are unwrapped and flattened:
274
275
```css
276
/* Input */
277
.component {
278
color: black;
279
280
@keyframes slide {
281
from { transform: translateX(-100%); }
282
to { transform: translateX(0); }
283
}
284
}
285
286
/* Output */
287
.component {
288
color: black;
289
}
290
291
@keyframes slide {
292
from { transform: translateX(-100%); }
293
to { transform: translateX(0); }
294
}
295
```
296
297
### At-Root Directive
298
Break out of nested contexts using `@at-root`:
299
300
```css
301
/* Input */
302
.page {
303
color: black;
304
305
.content {
306
padding: 20px;
307
308
@at-root {
309
.modal {
310
position: fixed;
311
top: 50%;
312
left: 50%;
313
}
314
}
315
}
316
}
317
318
/* Output */
319
.page {
320
color: black;
321
}
322
323
.page .content {
324
padding: 20px;
325
}
326
327
.modal {
328
position: fixed;
329
top: 50%;
330
left: 50%;
331
}
332
```
333
334
### At-Root with Selector
335
Use `@at-root` with a selector for targeted escaping:
336
337
```css
338
/* Input */
339
.theme {
340
.component {
341
color: blue;
342
343
@at-root html {
344
--primary-color: blue;
345
}
346
}
347
}
348
349
/* Output */
350
.theme .component {
351
color: blue;
352
}
353
354
html {
355
--primary-color: blue;
356
}
357
```
358
359
### At-Root with Filters
360
Control which rules escape using `with` and `without` filters:
361
362
```css
363
/* Input */
364
@media screen {
365
.component {
366
color: black;
367
368
@at-root (without: media) {
369
.global {
370
position: fixed;
371
}
372
}
373
}
374
}
375
376
/* Output */
377
@media screen {
378
.component {
379
color: black;
380
}
381
}
382
383
.global {
384
position: fixed;
385
}
386
```
387
388
## Configuration Examples
389
390
### Custom Bubble Rules
391
Add custom at-rules to bubble to the root:
392
393
```javascript
394
postcss([
395
postcssNested({
396
bubble: ['custom-query', 'my-rule']
397
})
398
])
399
```
400
401
```css
402
/* Input */
403
.element {
404
color: black;
405
406
@custom-query (min-width: 600px) {
407
color: blue;
408
}
409
}
410
411
/* Output */
412
.element {
413
color: black;
414
}
415
416
@custom-query (min-width: 600px) {
417
.element {
418
color: blue;
419
}
420
}
421
```
422
423
### Custom Unwrap Rules
424
Add custom at-rules to unwrap:
425
426
```javascript
427
postcss([
428
postcssNested({
429
unwrap: ['custom-keyframes']
430
})
431
])
432
```
433
434
### Preserve Empty Rules
435
Keep empty parent rules for CSS modules:
436
437
```javascript
438
postcss([
439
postcssNested({
440
preserveEmpty: true
441
})
442
])
443
```
444
445
```css
446
/* Input */
447
.parent {
448
.child {
449
color: red;
450
}
451
}
452
453
/* Output with preserveEmpty: true */
454
.parent {
455
}
456
457
.parent .child {
458
color: red;
459
}
460
461
/* Output with preserveEmpty: false (default) */
462
.parent .child {
463
color: red;
464
}
465
```
466
467
### Custom Root Rule Name
468
Use a custom name for the at-root directive:
469
470
```javascript
471
postcss([
472
postcssNested({
473
rootRuleName: 'escape'
474
})
475
])
476
```
477
478
```css
479
/* Input */
480
.nested {
481
color: black;
482
483
@escape {
484
.global {
485
position: fixed;
486
}
487
}
488
}
489
490
/* Output */
491
.nested {
492
color: black;
493
}
494
495
.global {
496
position: fixed;
497
}
498
```
499
500
## Error Handling
501
502
The plugin provides helpful error messages for common issues:
503
504
- **Syntax errors in selectors**: Reports missed semicolons and malformed selectors
505
- **Invalid at-root parameters**: Clear error messages for unrecognized parameter formats
506
- **Selector parsing failures**: Detailed error context when selectors cannot be parsed
507
508
## Dependencies
509
510
- **postcss**: ^8.2.14 (peer dependency)
511
- **postcss-selector-parser**: ^7.0.0 (direct dependency)
512
513
## Node.js Compatibility
514
515
- **Node.js**: >=18.0