gulp-css-morfemo
v1.1.12
Published
Gulp плагин расширяет возможности именования CSS-классов добавляя функционал к приставкам и суффиксам имен классов + фича с CSS-переменными
Maintainers
Readme
gulp-css-morfemo
Extend CSS classes like words in a language: root + prefixes + suffixes.
📌 Introduction
In real-world projects, you often have to duplicate styles for parent and child elements, write separate classes for each layer (color, layout, animation), and repeat CSS variable values in properties. This bloats the code and makes maintenance harder.
Solution: the gulp-css-morfemo plugin allows you to use word-formation when naming classes. You define a root (the base class name), and prefixes and suffixes automatically extend its behavior.
Everything is configurable, and all features can be turned off individually. The plugin analyzes your HTML and inserts the needed selectors directly into your CSS.
🚀 Features
- ✅ Suffixes — control nesting and sibling relationships through class names.
- ✅ Prefixes — separate CSS into layers (one letter + hyphen).
- ✅ Special
--*-selfvariables — automatically transform into properties. - ✅ Flexible configuration — each feature can be enabled/disabled separately.
- ✅ Custom suffix config — redefine rules for your own project.
- ✅ Auto HTML analysis — the plugin finds classes in HTML automatically.
- ✅ Selector merging — generated selectors are merged with the original rule (can be disabled).
📦 Installation
npm install gulp-css-morfemo --save-dev🛠 Использование
Минимальный пример gulpfile
const gulp = require('gulp');
const postcss = require('gulp-postcss');
const cssMorfemo = require('gulp-css-morfemo');
function cssTask() {
return gulp.src('src/main.css')
.pipe(postcss([
cssMorfemo({
htmlPath: 'src/index.html', // analyze classes from HTML
verbose: true // see what's happening
})
]))
.pipe(gulp.dest('dist/'));
}
exports.default = cssTask;Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| htmlPath | string | null | Path to HTML file for class analysis |
| suffixConfigPath | string | null | Path to custom suffix config |
| enableVariables | boolean | true | Process self variables |
| enablePrefixes | boolean | true | Process prefixes (layers) |
| enableSuffixes | boolean | true | Process suffixes |
| keepSelf | boolean | false | Keep original --*-self variables |
| combineSelectors | boolean | true | Merge generated selectors with the original rule (comma-separated). When false, creates separate rules |
| verbose | boolean | false | Show detailed console output |
🔍 Detailed Functionality
1. Suffixes
Control nesting and siblings directly through class names:
<div class="test-">
<div>Direct child</div>
</div>
<div class="test__">
<div>All descendants (any depth)</div>
</div>
<div class="test_">
<div>Next sibling</div>
</div>.test { color: red; }Result (default, combineSelectors: true):
.test, .test- > *, .test__ *, .test_ + * { color: red; }If you want separate rules, set combineSelectors: false.
🧮 Numeric suffixes (nth-child)
Work with element sequences without extra classes in HTML:
<div class="someclass-3n2">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
</div>.someclass { background: lightgray; }Result:
.someclass, .someclass-3n2 > *:nth-child(3n + 2) { background: lightgray; }The style applies to the 2nd, 5th, 8th… elements. The numbers 3 and 2 are just examples; you can use any numbers.
🔤 Aliases (syntactic sugar suffixes)
For common scenarios, there are handy aliases:
| Suffix | Selector |
|--------|----------|
| -even | > *:nth-child(even) |
| -odd | > *:nth-child(odd) |
| -first | > *:first-child |
| -last | > *:last-child |
| -not-first | > *:not(:first-child) |
| -not-empty | > *:not(:empty) |
<div class="list-even">
<div>1</div>
<div>2</div>
<div>3</div>
</div>.list { background: lightblue; }Result:
.list, .list-even > *:nth-child(even) { background: lightblue; }📋 Default suffix config
Here's the full list of suffixes available out of the box:
[
{ "suffix": "---", "match": "---$", "transform": ".${className}, .${className} *" },
{ "suffix": "--even", "match": "--even$", "transform": ".${className} *:nth-child(even)" },
{ "suffix": "--odd", "match": "--odd$", "transform": ".${className} *:nth-child(odd)" },
{ "suffix": "--xny", "match": "--(\\d+)n(\\d+)$", "transform": ".${className} *:nth-child(${1}n + ${2})" },
{ "suffix": "--xn", "match": "--(\\d+)n$", "transform": ".${className} *:nth-child(${1}n)" },
{ "suffix": "--x", "match": "--(\\d+)$", "transform": ".${className} *:nth-child(${1})" },
{ "suffix": "--first", "match": "--first$", "transform": ".${className} *:first-child" },
{ "suffix": "--last", "match": "--last$", "transform": ".${className} *:last-child" },
{ "suffix": "--not-empty", "match": "--not-empty$", "transform": ".${className} *:not(:empty)" },
{ "suffix": "--empty", "match": "--empty$", "transform": ".${className} *:empty" },
{ "suffix": "--", "match": "--$", "transform": ".${className} *" },
{ "suffix": "-even", "match": "-even$", "transform": ".${className} > *:nth-child(even)" },
{ "suffix": "-odd", "match": "-odd$", "transform": ".${className} > *:nth-child(odd)" },
{ "suffix": "-xny", "match": "-(\\d+)n(\\d+)$", "transform": ".${className} > *:nth-child(${1}n + ${2})" },
{ "suffix": "-xn", "match": "-(\\d+)n$", "transform": ".${className} > *:nth-child(${1}n)" },
{ "suffix": "-x", "match": "-(\\d+)$", "transform": ".${className} > *:nth-child(${1})" },
{ "suffix": "-first", "match": "-first$", "transform": ".${className} > *:first-child" },
{ "suffix": "-last", "match": "-last$", "transform": ".${className} > *:last-child" },
{ "suffix": "-not-first", "match": "-not-first$", "transform": ".${className} > *:not(:first-child)" },
{ "suffix": "-not-last", "match": "-not-last$", "transform": ".${className} > *:not(:last-child)" },
{ "suffix": "-not-empty", "match": "-not-empty$", "transform": ".${className} > *:not(:empty)" },
{ "suffix": "-empty", "match": "-empty$", "transform": ".${className} > *:empty" },
{ "suffix": "-", "match": "-$", "transform": ".${className} > *" },
{ "suffix": "__", "match": "__$", "transform": ".${className} ~ *" },
{ "suffix": "_", "match": "_$", "transform": ".${className} + *" }
]Field explanations:
suffix— the suffix itself.match— regular expression to detect the suffix in the class name.transform— CSS selector template where the full class name (${className}) is substituted.- The order of elements determines priority (first = highest priority).
Custom suffix config
If the built-in config doesn't suit you, you can create your own JSON file (an array of objects with the same fields) and point to it via the suffixConfigPath option. The structure exactly matches the default config above. The order of elements determines priority (first = highest priority).
A custom config is useful when you want to:
- Change existing suffixes (e.g., replace
-with_child). - Add your own suffixes.
- Remove unnecessary suffixes.
2. Prefixes (layers)
Separate CSS into logical layers: l- (layout), c- (color), a- (animation), t- (typography), s- (state). This helps keep the code organized.
.l-test { display: flex; gap: 10px; }
.c-test { color: red; background: blue; }Result: the plugin creates a .test class that combines both layers:
.test, .l-test, .c-test {
display: flex;
gap: 10px;
color: red;
background: blue;
}Now in HTML you can use .test instead of l-test c-test. If you don't need a particular layer, simply don't write it in CSS. Which letter represents which layer is up to you.
3. self variables
An ergonomic way to pass property values via CSS variables.
.test {
--color-self: red;
}Result: the plugin transforms this into:
.test {
--color: red;
color: var(--color);
}The --color-self variable is removed (if keepSelf: false), and the color property gets its value via var(--color). This is handy when you need to inherit a value through the cascade.
📄 Full build example (with watch and browserSync)
const gulp = require('gulp');
const postcss = require('gulp-postcss');
const browserSync = require('browser-sync').create();
const cssMorfemo = require('gulp-css-morfemo');
function cssTask() {
return gulp.src('src/main.css')
.pipe(postcss([
cssMorfemo({
htmlPath: 'src/index.html',
keepSelf: false,
combineSelectors: true, // can be omitted (default is true)
verbose: true
})
]))
.pipe(gulp.dest('dist/'));
}
function htmlTask() {
return gulp.src('src/index.html')
.pipe(gulp.dest('dist/'));
}
function watch() {
browserSync.init({
server: { baseDir: 'dist/' },
port: 3000,
notify: false,
open: true
});
gulp.watch('src/main.css', gulp.series(
cssTask,
(done) => { browserSync.reload(); done(); }
));
gulp.watch('src/index.html', gulp.series(
cssTask, // rebuild CSS with new classes
htmlTask,
(done) => { browserSync.reload(); done(); }
));
}
exports.default = gulp.series(
cssTask,
htmlTask,
watch
);❓ Why series, not parallel?
The plugin analyzes the HTML file (htmlPath) before processing CSS. So it's important to copy/generate the HTML (or update it) first, then run the CSS task. Use gulp.series to guarantee the correct order.
🤝 Support the project
This plugin was born from a personal project and the need for flexible CSS workflows. If you find it useful, please consider supporting its development:
📬 Ideas, bugs, suggestions — send me a personal message on GitHub.
📄 License
MIT © Anarkio
