Skip to content

Plugin best practices

Romain Menke edited this page Jul 9, 2022 · 5 revisions

A set of guidelines and tips to create plugins that work well for as much users as possible.

These are not in any particular order.

Selector and Value parsers

It is tempting to use string find/replace or regular expression operations to modify CSS in a plugin. These are easy ways that work well for simple cases.

However they fail when bits have escape sequences (\) or maybe they are part of a string.

/* looking for selector `:fancy-selector` */

:fancy-selector {}

\:fancy-selector {} /* escaping changes ":" */

[data-example=":fancy-selector"] {} /* strings in attribute selectors */
/* looking for value `fancy-value` */

.example {
  order: fancy-value; /* keyword value */
}

.example {
  content: "fancy-value"; /* string value */
}

Avoid regular expressions

A lot of people can do amazing things with regular expressions. We have however found that these create barriers and form maintenance issue.

If the regular expression is simple it will most likely also have a simple string operation that is more expressive.

// you need to know about the `i` flag
/foo/i.test("fOO")

vs.

// you can read the code and understand everything without prior knowledge
"fOO".toLowerCase() === "foo"

If the regular expression is complex it will not be maintainable. The chances that someone in the future needs to make a change and does not fully understand your regular expression is non-zero. We prefer to use more expressive API when these are available.

Fast aborts before further parsing

Often it is possible to have a simple and fast check to verify that a PostCSS definitely does not need to be processed. If you only want to act on :has(...) selectors you can look for :has(.

If this is not found you do not need to pass the selector to postcss-selector-parser. The early return pattern also helps to keep your code clean and readable.

var lowerCaseSelector = rule.selector.toLowerCase();
if ( ! lowerCaseSelector.includes(':has(') ) {
	return;
}

Case (in)sensitivity

CSS is very specific about case sensitivity in keywords, properties and selectors. Be aware of which bits are case sensitive or insensitive when detecting parts to replace and/or modify.

these are all valid :

:FOCUS {
 color: currentColor;
}

:Focus {
 Color: CurrentColor;
}

PostCSS 8 visitors

For most plugins we want to avoid Once and OnceExit. These however are not forbidden and some plugins can only work correctly at the start or end of processing.

Using the correct one is important in the context of postcss-preset-env. There is no golden rule here that fixes all issues.

Using a broader or more specific visitor will effect the order of execution:

Declaration(decl) {
	// runs together with other `Declaration` visitors
	// in the order of adding plugins 
}
Declaration: {
	color(colorDecl) {
		// runs together with other `Declaration:color` visitors
		// in the order of adding plugins 
		// ! After all `Declaration` visitors
	}
}

Always clone modified nodes

Only changing a property of a PostCSS node will not trigger other plugins to revisit this node. Make sure to always clone nodes if aspects are modified.

// Always clone
decl.cloneBefore({
	value: modifiedValue,
});

// Optionally remove the original
if (!options.preserve) {
	decl.remove();
}

Custom property fallbacks

Custom properties can't have fallbacks as those we know from non-custom properties. Custom properties become valid/invalid differently and in plugins we need to take extra measures not to break things for users.

/* does not work */
:root {
	--foo: red; /* will never be used */
	--foo: color(display-p3 1 0 0);
}

vs.

/* works */
:root {
	--foo: red; /* will never be used */
}

@supports (color: color(display-p3 1 0 0)) {
	/* conditionally redefine the custom property */
	:root {
		--foo: color(display-p3 1 0 0);
	}
}

Manual fallback declarations

Some users or another plugin might already provide fallbacks. In those cases we do not want to add an extra fallback and assume that the existing fallbacks are better.

.example {
	color: red; /* existing fallback */
	color: color(display-p3 1 0 0); /* should be ignored by plugins */
}

This also serves as an escape hatch for users. If there is a bug in a plugin they can manually set a fallback and the plugin should then ignore this bit.

@supports ancestors

If a user is manually adding @supports rules around CSS a plugin could transform it should be ignored just the same as manually written fallbacks.

@supports (color: color(display-p3 1 0 0)) {
	.example {
		color: color(display-p3 1 0 0); /* should be ignored by plugins */
	}
}