Effiziente unendliche Utility-Helfer mit Inline-CSS-Custom-Properties und calc()

Avatar of Andy Ford
Andy Ford am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Kürzlich habe ich eine sehr einfache Sass-Schleife geschrieben, die mehrere Padding- und Margin-Utility-Klassen ausgibt. Nichts Besonderes, wirklich, nur eine Sass-Map mit 11 Abstands-Werten, die durchlaufen wird, um Klassen für Padding und Margin auf jeder Seite zu erstellen. Wie wir sehen werden, funktioniert das, aber es führt zu einer ziemlich beträchtlichen Menge an CSS. Wir werden es neu gestalten, um CSS Custom Properties zu verwenden und das System viel schlanker zu machen.

Hier ist die ursprüngliche Sass-Implementierung

$space-stops: (
  '0': 0,
  '1': 0.25rem,
  '2': 0.5rem,
  '3': 0.75rem,
  '4': 1rem,
  '5': 1.25rem,
  '6': 1.5rem,
  '7': 1.75rem,
  '8': 2rem,
  '9': 2.25rem,
  '10': 2.5rem,
);

@each $key, $val in $space-stops {
  .p-#{$key} {
    padding: #{$val} !important;
  }
  .pt-#{$key} {
    padding-top: #{$val} !important;
  }
  .pr-#{$key} {
    padding-right: #{$val} !important;
  }
  .pb-#{$key} {
    padding-bottom: #{$val} !important;
  }
  .pl-#{$key} {
    padding-left: #{$val} !important;
  }
  .px-#{$key} {
    padding-right: #{$val} !important;
    padding-left: #{$val} !important;
  }
  .py-#{$key} {
    padding-top: #{$val} !important;
    padding-bottom: #{$val} !important;
  }

  .m-#{$key} {
    margin: #{$val} !important;
  }
  .mt-#{$key} {
    margin-top: #{$val} !important;
  }
  .mr-#{$key} {
    margin-right: #{$val} !important;
  }
  .mb-#{$key} {
    margin-bottom: #{$val} !important;
  }
  .ml-#{$key} {
    margin-left: #{$val} !important;
  }
  .mx-#{$key} {
    margin-right: #{$val} !important;
    margin-left: #{$val} !important;
  }
  .my-#{$key} {
    margin-top: #{$val} !important;
    margin-bottom: #{$val} !important;
  }
}

Das funktioniert sehr gut. Es gibt alle benötigten Utility-Klassen aus. Aber es kann auch schnell aufblähen. In meinem Fall waren es etwa 8,6 KB unkomprimiert und unter 1 KB komprimiert. (Brotli war 542 Bytes und Gzip kam auf 925 Bytes.)

Da sie extrem repetitiv sind, komprimieren sie gut, aber ich konnte das Gefühl nicht loswerden, dass all diese Klassen überflüssig waren. Außerdem hatte ich noch nicht einmal kleine/mittlere/große Breakpoints hinzugefügt, die für solche Helferklassen ziemlich typisch sind.

Hier ist ein künstliches Beispiel dafür, wie die responsive Version mit zusätzlichen Klassen für klein/mittel/groß aussehen könnte. Wir werden die zuvor definierte $space-stops Map wiederverwenden und unseren repetitiven Code in ein Mixin packen

@mixin finite-spacing-utils($bp: '') {
    @each $key, $val in $space-stops {
        .p-#{$key}#{$bp} {
            padding: #{$val} !important;
        }
        .pt-#{$key}#{$bp} {
            padding-top: #{$val} !important;
        }
        .pr-#{$key}#{$bp} {
            padding-right: #{$val} !important;
        }
        .pb-#{$key}#{$bp} {
            padding-bottom: #{$val} !important;
        }
        .pl-#{$key}#{$bp} {
            padding-left: #{$val} !important;
        }
        .px-#{$key}#{$bp} {
            padding-right: #{$val} !important;
            padding-left: #{$val} !important;
        }
        .py-#{$key}#{$bp} {
            padding-top: #{$val} !important;
            padding-bottom: #{$val} !important;
        }

        .m-#{$key}#{$bp} {
            margin: #{$val} !important;
        }
        .mt-#{$key}#{$bp} {
            margin-top: #{$val} !important;
        }
        .mr-#{$key}#{$bp} {
            margin-right: #{$val} !important;
        }
        .mb-#{$key}#{$bp} {
            margin-bottom: #{$val} !important;
        }
        .ml-#{$key}#{$bp} {
            margin-left: #{$val} !important;
        }
        .mx-#{$key}#{$bp} {
            margin-right: #{$val} !important;
            margin-left: #{$val} !important;
        }
        .my-#{$key}#{$bp} {
            margin-top: #{$val} !important;
            margin-bottom: #{$val} !important;
        }
    }
}

@include finite-spacing-utils;

@media (min-width: 544px) {
    @include finite-spacing-utils($bp: '_sm');
}

@media (min-width: 768px) {
    @include finite-spacing-utils($bp: '_md');
}

@media (min-width: 1024px) {
    @include finite-spacing-utils($bp: '_lg');
}

Das sind etwa 41,7 KB unkomprimiert (und etwa 1 KB mit Brotli und 3 KB mit Gzip). Es komprimiert immer noch gut, aber es ist ein bisschen lächerlich.

Ich wusste, dass es möglich war, data-* Attribute aus CSS mit der [attr() Funktion zu referenzieren. Daher fragte ich mich, ob es möglich wäre, calc() und attr() zusammen zu verwenden, um dynamisch berechnete Abstands-Utility-Helfer über data-* Attribute zu erstellen - wie data-m="1" oder data-m="1@md" - und dann im CSS etwas wie margin: calc(attr(data-m) * 0.25rem) zu tun (vorausgesetzt, ich verwende eine Abstandsskala, die in 0.25rem Intervallen inkrementiert). Das könnte sehr mächtig sein.

Aber das Ende dieser Geschichte ist: Nein, Sie können (derzeit) attr() mit keiner anderen Eigenschaft als der content Eigenschaft verwenden. Schade. Aber bei der Suche nach Informationen zu attr() und calc() habe ich diesen faszinierenden Stack Overflow-Kommentar von Simon Rigét gefunden, der vorschlägt, eine CSS-Variable direkt innerhalb eines Inline-Style-Attributs zu setzen. Aha!

Es ist also möglich, etwas wie <div style="--p: 4;"> zu tun und dann in CSS

:root {
  --p: 0;
}

[style*='--p:'] {
  padding: calc(0.25rem * var(--p)) !important;
}

Im Fall des Beispiels style="--p: 4;" würden Sie effektiv padding: 1rem !important; erhalten.

… und jetzt haben Sie eine unendlich skalierbare Abstands-Utility-Klasse Monstrum Helfer.

So könnte das in CSS aussehen

:root {
  --p: 0;
  --pt: 0;
  --pr: 0;
  --pb: 0;
  --pl: 0;
  --px: 0;
  --py: 0;
  --m: 0;
  --mt: 0;
  --mr: 0;
  --mb: 0;
  --ml: 0;
  --mx: 0;
  --my: 0;
}

[style*='--p:'] {
  padding: calc(0.25rem * var(--p)) !important;
}
[style*='--pt:'] {
  padding-top: calc(0.25rem * var(--pt)) !important;
}
[style*='--pr:'] {
  padding-right: calc(0.25rem * var(--pr)) !important;
}
[style*='--pb:'] {
  padding-bottom: calc(0.25rem * var(--pb)) !important;
}
[style*='--pl:'] {
  padding-left: calc(0.25rem * var(--pl)) !important;
}
[style*='--px:'] {
  padding-right: calc(0.25rem * var(--px)) !important;
  padding-left: calc(0.25rem * var(--px)) !important;
}
[style*='--py:'] {
  padding-top: calc(0.25rem * var(--py)) !important;
  padding-bottom: calc(0.25rem * var(--py)) !important;
}

[style*='--m:'] {
  margin: calc(0.25rem * var(--m)) !important;
}
[style*='--mt:'] {
  margin-top: calc(0.25rem * var(--mt)) !important;
}
[style*='--mr:'] {
  margin-right: calc(0.25rem * var(--mr)) !important;
}
[style*='--mb:'] {
  margin-bottom: calc(0.25rem * var(--mb)) !important;
}
[style*='--ml:'] {
  margin-left: calc(0.25rem * var(--ml)) !important;
}
[style*='--mx:'] {
  margin-right: calc(0.25rem * var(--mx)) !important;
  margin-left: calc(0.25rem * var(--mx)) !important;
}
[style*='--my:'] {
  margin-top: calc(0.25rem * var(--my)) !important;
  margin-bottom: calc(0.25rem * var(--my)) !important;
}

Das ist sehr ähnlich wie die erste Sass-Schleife oben, aber es gibt keine Schleife, die 11 Mal durchläuft - und trotzdem ist es unendlich. Es sind etwa 1,4 KB unkomprimiert, 226 Bytes mit Brotli oder 284 Bytes mit Gzip.

Wenn Sie dies für Breakpoints erweitern möchten, ist die unerfreuliche Nachricht, dass Sie das "@"-Zeichen nicht in CSS-Variablennamen einfügen können (obwohl Emojis und andere UTF-8-Zeichen seltsamerweise erlaubt sind). Sie könnten also wahrscheinlich Variablennamen wie p_sm oder sm_p einrichten. Sie müssten einige zusätzliche CSS-Variablen und einige Media Queries hinzufügen, um all das zu handhaben, aber es wird nicht exponentiell explodieren, wie es traditionelle CSS-Klassennamen tun, die mit einer Sass-For-Schleife erstellt wurden.

Hier ist die äquivalente responsive Version. Wir verwenden wieder ein Sass-Mixin, um die Wiederholung zu reduzieren

:root {
  --p: 0;
  --pt: 0;
  --pr: 0;
  --pb: 0;
  --pl: 0;
  --px: 0;
  --py: 0;
  --m: 0;
  --mt: 0;
  --mr: 0;
  --mb: 0;
  --ml: 0;
  --mx: 0;
  --my: 0;
}

@mixin infinite-spacing-utils($bp: '') {
    [style*='--p#{$bp}:'] {
        padding: calc(0.25rem * var(--p#{$bp})) !important;
    }
    [style*='--pt#{$bp}:'] {
        padding-top: calc(0.25rem * var(--pt#{$bp})) !important;
    }
    [style*='--pr#{$bp}:'] {
        padding-right: calc(0.25rem * var(--pr#{$bp})) !important;
    }
    [style*='--pb#{$bp}:'] {
        padding-bottom: calc(0.25rem * var(--pb#{$bp})) !important;
    }
    [style*='--pl#{$bp}:'] {
        padding-left: calc(0.25rem * var(--pl#{$bp})) !important;
    }
    [style*='--px#{$bp}:'] {
        padding-right: calc(0.25rem * var(--px#{$bp})) !important;
        padding-left: calc(0.25rem * var(--px)#{$bp}) !important;
    }
    [style*='--py#{$bp}:'] {
        padding-top: calc(0.25rem * var(--py#{$bp})) !important;
        padding-bottom: calc(0.25rem * var(--py#{$bp})) !important;
    }
    [style*='--m#{$bp}:'] {
        margin: calc(0.25rem * var(--m#{$bp})) !important;
    }
    [style*='--mt#{$bp}:'] {
        margin-top: calc(0.25rem * var(--mt#{$bp})) !important;
    }
    [style*='--mr#{$bp}:'] {
        margin-right: calc(0.25rem * var(--mr#{$bp})) !important;
    }
    [style*='--mb#{$bp}:'] {
        margin-bottom: calc(0.25rem * var(--mb#{$bp})) !important;
    }
    [style*='--ml#{$bp}:'] {
        margin-left: calc(0.25rem * var(--ml#{$bp})) !important;
    }
    [style*='--mx#{$bp}:'] {
        margin-right: calc(0.25rem * var(--mx#{$bp})) !important;
        margin-left: calc(0.25rem * var(--mx#{$bp})) !important;
    }
    [style*='--my#{$bp}:'] {
        margin-top: calc(0.25rem * var(--my#{$bp})) !important;
        margin-bottom: calc(0.25rem * var(--my#{$bp})) !important;
    }
}

@include infinite-spacing-utils;

@media (min-width: 544px) {
    @include infinite-spacing-utils($bp: '_sm');
}

@media (min-width: 768px) {
    @include infinite-spacing-utils($bp: '_md');
}

@media (min-width: 1024px) {
    @include infinite-spacing-utils($bp: '_lg');
}

Das sind etwa 6,1 KB unkomprimiert, 428 Bytes mit Brotli und 563 mit Gzip.

Glauben Sie, dass das Schreiben von HTML wie <div style="--px:2; --my:4;"> gefällig für das Auge oder gut für die Entwicklerergonomie ist... nein, nicht besonders. Aber könnte dieser Ansatz in Situationen, in denen Sie (aus irgendeinem Grund) extrem minimales CSS benötigen, oder vielleicht gar keine externe CSS-Datei, praktikabel sein? Ja, das tue ich sicher.

Es sei hier darauf hingewiesen, dass CSS-Variablen, die in Inline-Stilen zugewiesen werden, nicht nach außen dringen. Sie sind nur auf das aktuelle Element beschränkt und ändern den Wert der Variablen nicht global. Gott sei Dank! Die einzige Seltsamkeit, die ich bisher gefunden habe, ist, dass DevTools (zumindest in Chrome, Firefox und Safari) die mit dieser Technik verwendeten Stile nicht im Tab "Computed Styles" anzeigen.

Es ist auch erwähnenswert, dass ich die guten alten padding  und margin Eigenschaften mit -top, -right, -bottom und -left verwendet habe, aber Sie könnten die äquivalenten logischen Eigenschaften wie padding-block und padding-inline verwenden. Es ist sogar möglich, ein paar Bytes einzusparen, indem man logische Eigenschaften selektiv mit traditionellen Eigenschaften mischt und abstimmt. Ich habe es auf diese Weise auf 400 Bytes mit Brotli und 521 mit Gzip reduziert.

Andere Anwendungsfälle

Dies scheint am besten für Dinge geeignet zu sein, die auf einer (linearen) inkrementellen Skala basieren (weshalb Padding und Margin ein guter Anwendungsfall zu sein scheinen), aber ich kann mir vorstellen, dass dies potenziell für Breiten und Höhen in Grid-Systemen (Spaltennummern und/oder Breiten) funktionieren könnte. **Vielleicht** für typografische Skalen (aber vielleicht auch nicht).

Ich habe mich viel auf die Dateigröße konzentriert, aber es gibt vielleicht noch andere Verwendungszwecke, an die ich nicht denke. Vielleicht würden **Sie** Ihren Code nicht auf diese Weise schreiben, aber ein kritisches CSS-Tool könnte den Code potenziell refaktorieren, um diesen Ansatz zu verwenden.

Tiefer graben

Als ich tiefer grub, fand ich heraus, dass Ahmad Shadeed 2019 darüber bloggte, calc() mit CSS-Variablenzuweisungen innerhalb von Inline-Stilen zu mischen, insbesondere für Avatar-Größen. Miriam Suzannes Artikel auf Smashing Magazine von 2019 verwendete zwar nicht calc(), zeigte aber einige erstaunliche Dinge, die man mit Variablenzuweisungen in Inline-Stilen machen kann.