Skip to content

PostCSS Logical Issues

Romain Menke edited this page Mar 22, 2023 · 1 revision

postcss-logical-properties worked in a very specific way that lead to multiple issues for users. How that plugin worked however was also a very powerful feature for many users.

This wiki page is intended to record the classes of bugs and why they happened.


Biased against vertical script

With [dir] and :dir() we can not support vertical script. That means that postcss-logical becomes a barrier for adoption for specific languages, regions, cultures. We never intended this and don't think this barrier should exist.

By not using [dir] or :dir() and adding support for vertical script we removed this bias from the plugin.


Combinatorial explosion

reported in : https://github.com/csstools/postcss-plugins/issues/619

:root {
  --size: 1rem;
}

.stage__container {
  inset-inline-start: var(--size);
}

becomes :

:root {
  --size: 1rem;
}

[dir="ltr"] .stage__container {
  left: 1rem;
  left: var(--size);
}

[dir="ltr"] .stage__container {
  left: 1rem;
  left: 1rem;
  left: var(--size);
}

.stage__container:dir(ltr) {
  left: 1rem;
  left: var(--size);
}

[dir="rtl"] .stage__container {
  right: 1rem;
  right: var(--size);
}

[dir="rtl"] .stage__container {
  right: 1rem;
  right: 1rem;
  right: var(--size);
}

.stage__container:dir(rtl) {
  right: 1rem;
  right: var(--size);
}

[dir="ltr"] .stage__container {
  left: 1rem;
}

.stage__container:dir(ltr) {
  left: 1rem;
}

[dir="rtl"] .stage__container {
  right: 1rem;
}

.stage__container:dir(rtl) {
  right: 1rem;
}

.stage__container {
  inset-inline-start: 1rem;
  inset-inline-start: var(--size);
}

This bug happens because multiple transforms might be needed to fallback a declaration. When a plugin like postcss-logical produces multiple new rules all declarations therein will also be processed again and again by other plugins.

The end result is extreme bloat and broken CSS.


Specificity increase

reported in : https://github.com/csstools/postcss-plugins/issues/90

.foo {
  margin-inline-start: 10px;
}

/* is transformed into: */

.foo:dir(ltr) {
  margin-left: 10px;
}

.foo:dir(rtl) {
  margin-right: 10px;
}

.foo:dir(ltr) is more specific than .foo. [0 2 0] vs. [0 1 0] This leads to subtle bugs.

For example :

li:hover { /* [0 1 1] */
  margin-inline-start: 15px;
}

.list-item { /* [0 1 0] */
  margin-inline-start: 5px;
}

/* is transformed into: */

li:hover { /* [0 1 1] */
  margin-inline-start: 15px;
}

.list-item:dir(ltr) { /* [0 2 0] */
  margin-left: 5px;
}

.list-item:dir(rtl) { /* [0 2 0] */
  margin-right: 5px;
}

This is definitely an artificial example but it does illustrate how the plugin altered CSS.

  • before transforming li:hover could match <li class="list-item">.
  • after transforming li:hover would never match <li class="list-item">.

Property inversion

.foo {
  margin-inline-start: 10px;
}

/* is transformed into: */

.foo {
  margin-left: 10px;
  margin-inline-start: 10px;
}

left and inline-start are only equivalent when the writing mode is horizontal and left to right.

Right to left would render as :

.foo {
  margin-left: 10px;
  margin-right: 10px;
}

preserve is intended for transforms that aren't an exact equivalent. But in this case the preserved declaration contradicts the fallback in certain contexts.