Ansätze für Media Queries in Sass

Avatar of Eduardo Bouças
Eduardo Bouças am

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

Die Verwendung von Media Queries in CSS als Teil von responsiven Websites ist für heutige Frontend-Entwickler alltäglich. Die Verwendung von Präprozessoren, um sie komfortabler zu schreiben und leichter zu warten, ist ebenfalls üblich geworden.

Ich habe einige Monate lang mit einem Dutzend verschiedener Ansätze für Media Queries in Sass experimentiert und einige davon tatsächlich produktiv eingesetzt. Alle erwiesen sich letztendlich als ungeeignet, um alles, was ich auf elegante Weise tun musste, abzudecken. Daher habe ich das, was mir an jedem einzelnen gefiel, genommen und eine Lösung entwickelt, die alle aufgetretenen Szenarien abdeckt.

Warum überhaupt einen Präprozessor verwenden?

Das ist eine berechtigte Frage. Was bringt es schließlich, all das zu tun, wenn man Media Queries auch einfach mit purem CSS schreiben kann? Sauberkeit und Wartbarkeit.

Der häufigste Anwendungsfall für Media Queries ist die Transformation eines Layouts basierend auf der Breite des Browser-Viewports. Sie können ein Layout so anpassen, dass mehrere Geräte mit unterschiedlichen Bildschirmgrößen ein optimales Erlebnis genießen können. Infolgedessen werden die Ausdrücke, die zur Definition der Media Queries verwendet werden, sich auf die typische Bildschirmbreite dieser Geräte beziehen.

Wenn Ihr Code also 5 Media Queries enthält, die sich an Tablet-Geräte mit einer Breite von 768px richten, werden Sie diese Zahl 5 Mal fest codieren, was etwas Hässliches ist, das meine OCD niemals verzeihen würde. Erstens möchte ich, dass mein Code so einfach zu lesen ist, dass jeder sofort versteht, dass eine Media Query sich an Tablet-Geräte richtet, nur durch das Hinsehen – ich glaube, das Wort Tablet würde das besser tun als 768px.

Was ist, wenn sich diese Referenzbreite in Zukunft ändert? Ich hasse die Vorstellung, sie an 5 Stellen im Code ersetzen zu müssen, besonders wenn sie über mehrere Dateien verteilt ist.

Ein erster Schritt wäre, diesen Breakpoint in einer Variablen zu speichern und diese zur Konstruktion der Media Query zu verwenden.

/* Using plain CSS */
@media (min-width: 768px) {
  
}

/* Using SCSS variables to store breakpoints */
$breakpoint-tablet: 768px;
@media (min-width: $breakpoint-tablet) {
  
}

Ein weiterer Grund, Media Queries mit einem Präprozessor wie Sass zu schreiben, ist, dass er manchmal wertvolle Hilfe bei der Syntax leisten kann, insbesondere beim Schreiben eines Ausdrucks mit einem logischen oder (dargestellt durch ein Komma in CSS).

Wenn Sie zum Beispiel Retina-Geräte ansprechen möchten, wird die reine CSS-Syntax etwas umständlich.

/* Plain CSS */
@media (min-width: 768px) and 
       (-webkit-min-device-pixel-ratio: 2), 
       (min-width: 768px) and 
       (min-resolution: 192dpi) { 
}

/* Using variables? */
@media (min-width: $bp-tablet) and ($retina) { // or #{$retina}

}

Es sieht zwar besser aus, funktioniert aber leider nicht wie erwartet.

Ein Problem mit der Logik

Aufgrund der Funktionsweise des CSS-"oder"-Operators könnte ich die Retina-Bedingungen nicht mit anderen Ausdrücken mischen, da a (b or c) zu (a or b) c kompiliert würde und nicht zu a b or a c.

$retina: "(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)";
 
// This will generate unwanted results!
@media (min-width: 480px) and #{$retina} {
  body {
    background-color: red;
  }
}
/* Not the logic we're looking for */
@media (min-width: 480px) and (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
  body {
    background-color: red;
  }
}

Ich erkannte, dass ich etwas Mächtigeres brauchte, wie eine Mixin oder eine Funktion, um dies zu lösen. Ich habe ein paar Lösungen ausprobiert.

Dmitry Sheikos Technik

Eine, die ich ausprobiert habe, war Dmitry Sheikos Technik, die eine schöne Syntax hatte und Chris' Retina-Deklaration enthielt.

// Predefined Break-points
$mediaMaxWidth: 1260px;
$mediaBp1Width: 960px;
$mediaMinWidth: 480px;

@function translate-media-condition($c) {
  $condMap: (
    "screen": "only screen",
    "print": "only print",
    "retina": "(-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (min-device-pixel-ratio: 1.5), (min-resolution: 120dpi)",
    ">maxWidth": "(min-width: #{$mediaMaxWidth + 1})",
    "<maxWidth": "(max-width: #{$mediaMaxWidth})", 		
    ">bp1Width": "(min-width: #{$mediaBp1Width + 1})",
    "<bp1Width": "(max-width: #{$mediaBp1Width})",
    ">minWidth": "(min-width: #{$mediaMinWidth + 1})",
    "<minWidth": "(max-width: #{$mediaMinWidth})"
  );
  @return map-get( $condMap, $c );
}

// The mdia mixin
@mixin media($args...) {
  $query: "";
  @each $arg in $args {
    $op: "";
    @if ( $query != "" ) {
      $op: " and ";
    }
    $query: $query + $op + translate-media-condition($arg);
  }
  @media #{$query}  { @content; }
}

Aber das Problem mit der logischen Disjunktion bestand weiterhin.

.section {
  @include media("retina", "<minWidth") {
    color: white;
  };
}
/* Not the logic we're looking for */
@media (-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3 / 2), (min-device-pixel-ratio: 1.5), (min-resolution: 120dpi) and (max-width: 480px) {
  .section {
    background: blue;
    color: white;
  }
}

Landon Schropps Technik

Landon Schropps war mein nächster Stopp. Landon erstellt einfache benannte Mixins, die spezifische Aufgaben erfüllen. Wie

$tablet-width: 768px;
$desktop-width: 1024px;

@mixin tablet {
  @media (min-width: #{$tablet-width}) and (max-width: #{$desktop-width - 1px}) {
    @content;
  }
}

@mixin desktop {
  @media (min-width: #{$desktop-width}) {
    @content;
  }
}

Er hat auch eine Single-Responsibility-Retina-Version.

Aber ein weiteres Problem trat auf, als ich ein Element gestaltete, das zusätzliche Regeln für Zwischen-Breakpoints erforderte. Ich wollte meine Liste globaler Breakpoints nicht mit fallspezifischen Werten verunreinigen, nur um die Mixin weiterhin verwenden zu können, aber ich wollte definitiv nicht auf die Mixin verzichten und wieder auf reines CSS zurückfallen und jedes Mal, wenn ich benutzerdefinierte Werte verwenden musste, Dinge fest codieren.

/* I didn't want to sometimes have this */
@include tablet {

}

/* And other times this */
@media (min-width: 768px) and (max-width: 950px) {

}

Breakpoint-Technik

Breakpoint-sass war als nächstes auf meiner Liste, da es sowohl Variablen als auch benutzerdefinierte Werte in seiner Syntax unterstützt (und als Bonus ist es wirklich clever mit Pixel-Ratio-Media-Queries).

Ich konnte etwas schreiben wie

$breakpoint-tablet: 768px;

@include breakpoint(453px $breakpoint-tablet) {

}

@include breakpoint($breakpoint-tablet 850px) {

}

/* Compiles to: */
@media (min-width: 453px) and (max-width: 768px) {

}

@media (min-width: 768px) and (max-width: 850px) {

}

Die Dinge sahen besser aus, aber ich persönlich finde, dass die Syntax von Breakpoint-sass weniger natürlich ist als die von Dmitry. Sie können eine Zahl angeben und es wird angenommen, dass es ein min-width-Wert ist, oder eine Zahl und eine Zeichenfolge, und es wird angenommen, dass es sich um ein Eigenschaft/Wert-Paar handelt, um nur einige der unterstützten Kombinationen zu nennen.

Das ist in Ordnung, und ich bin sicher, es funktioniert gut, sobald man sich daran gewöhnt hat, aber ich hatte die Hoffnung noch nicht aufgegeben, eine Syntax zu finden, die sowohl einfach als auch so nah wie möglich an der Art und Weise ist, wie ich mündlich beschreibe, was eine Media Query ansprechen muss.

Auch wenn Sie sich das obige Beispiel ansehen, werden Sie sehen, dass ein Gerät mit einer Breite von genau 768px beide Media Queries auslöst, was möglicherweise nicht genau das ist, was wir wollen. Daher habe ich die Möglichkeit, inklusive und exklusive Breakpoints zu schreiben, meiner Anforderungsliste hinzugefügt.

Meine (Eduardo Boucass') Technik

Das ist mein Beitrag dazu.

Saubere Syntax, dynamische Deklaration

Ich bin ein Fan von Dmitrys Syntax, daher war meine Lösung davon inspiriert. Ich hätte jedoch gerne mehr Flexibilität bei der Erstellung von Breakpoints. Anstatt die Namen der Breakpoints fest in der Mixin zu codieren, habe ich eine mehrdimensionale Map verwendet, um sie zu deklarieren und zu labeln.

$breakpoints: (phone: 640px, 
               tablet: 768px, 
               desktop: 1024px) !default;

@include media(">phone", "<tablet") {
}

@include media(">tablet", "<950px") {
}

Die Mixin kommt mit einer Reihe von Standard-Breakpoints, die Sie an jeder Stelle im Code überschreiben können, indem Sie die Variable $breakpoints neu deklarieren.

Inklusive und exklusive Breakpoints

Ich wollte eine feinere Kontrolle über die Intervalle in den Ausdrücken haben, daher habe ich die Unterstützung für die Operatoren kleiner-als-oder-gleich und größer-als-oder-gleich aufgenommen. Auf diese Weise kann ich dieselbe Breakpoint-Deklaration in zwei sich gegenseitig ausschließenden Media Queries verwenden.

@include media(">=phone", "<tablet") {

}

@include media(">=tablet", "<=950px") {

}

/* Compiles to */
@media (min-width: 640px) and (max-width: 767px) {

}

@media (min-width: 768px) and (max-width: 950px) {

}

Medientypen ableiten und logische Disjunktion behandeln

Ähnlich wie bei den Breakpoints gibt es standardmäßig eine Liste für Medientypen und andere statische Ausdrücke (die Sie überschreiben können, indem Sie die Variable $media-expressions festlegen). Dies fügt Unterstützung für optionale Medientypen hinzu, wie screen oder handheld, ist aber auch in der Lage, Ausdrücke mit logischen Disjunktionen korrekt zu behandeln, wie die zuvor gesehene Retina-Media-Query. Die Disjunktionen werden als verschachtelte Listen von Zeichenfolgen deklariert.

$media-expressions: (screen: "screen", 
                    handheld: "handheld",
                    retina2x: 
                    ("(-webkit-min-device-pixel-ratio: 2)", 
                    "(min-resolution: 192dpi)")) !default;

@include media("screen", ">=tablet") {

}

@include media(">tablet", "<=desktop", "retina2x") {

}

/* Compiles to */
@media screen and (min-width: 768px) {

}

@media (min-width: 769px) and 
       (max-width: 1024px) and 
       (-webkit-min-device-pixel-ratio: 2),
       (min-width: 769px) and 
       (max-width: 1024px) and 
       (min-resolution: 192dpi) {

}

Es gibt keine Raketenwissenschaft dahinter, aber die vollständige Implementierung der Mixin ist nichts, was ich in nur wenigen Codezeilen zeigen könnte. Anstatt Sie mit riesigen Code-Schnipseln und endlosen Kommentaren zu langweilen, habe ich ein Pen mit allem, was funktioniert, beigefügt und werde den Prozess, den es durchläuft, um die Media Queries zu konstruieren, kurz beschreiben.

Wie es funktioniert

  1. Die Mixin empfängt mehrere Argumente als Zeichenfolgen und beginnt damit, jedes einzelne zu durchlaufen, um herauszufinden, ob es einen Breakpoint, eine benutzerdefinierte Breite oder einen der statischen Media-Ausdrücke darstellt.
  2. Wenn ein Operator gefunden wird, wird er extrahiert und jeder passende Breakpoint wird zurückgegeben, andernfalls nehmen wir an, dass es sich um einen benutzerdefinierten Wert handelt, und wandeln ihn in eine Zahl um (mithilfe von SassyCast).
  3. Wenn es sich um einen statischen Media-Ausdruck handelt, prüft er auf oder-Operatoren und generiert alle notwendigen Kombinationen, um die Disjunktion darzustellen.
  4. Der Prozess wird für alle Argumente wiederholt und die Ergebnisse werden mit dem und-Connector verknüpft, um den Media-Query-Ausdruck zu bilden.

Wenn Sie sich den vollständigen Sass-Code ansehen möchten, finden Sie ihn hier. Er heißt include-media auf GitHub.

Abschließende Gedanken

  • Ich bin ein großer Fan von dieser Technik, um Sass mit JavaScript sprechen zu lassen. Da wir Breakpoints als multidimensionale Liste mit ihren Namen als Schlüssel deklarieren, wird der Export in großen Mengen an JavaScript sehr einfach und kann mit nur wenigen Codezeilen automatisch erfolgen.
  • Ich versuche nicht, die Lösungen anderer Leute schlecht zu machen, und ich sage definitiv nicht, dass diese besser ist. Ich habe sie erwähnt, um einige der Hindernisse auf dem Weg zu meiner idealen Lösung aufzuzeigen, sowie einige großartige Dinge, die sie eingeführt haben und die meine eigene Lösung inspiriert haben.
  • Sie haben vielleicht einige Bedenken hinsichtlich der Länge und Komplexität dieser Implementierung. Während ich das verstehe, ist die Idee dahinter, dass Sie eine einzelne Datei herunterladen, sie in Ihr Projekt @importieren und mit der Verwendung beginnen, ohne den Quellcode anfassen zu müssen. Pingen Sie mich aber auf Twitter an, wenn Sie Fragen haben.
  • Sie können es von GitHub erhalten und sind herzlich eingeladen, mit Issues/Code/Liebe beizutragen. Ich bin sicher, es gibt noch viel, was wir tun können, um es besser zu machen.

Update!

Eduardo hat eine Website für seinen Ansatz erstellt: @include-media.