Passei alguns meses fazendo experiências com diferentes abordagens para escrever media queries simples, elegantes e fáceis de manter com o Sass. Cada solução tinha algo de que eu realmente gostava, mas não consegui encontrar uma que cobrisse tudo o que eu precisava fazer, então me aventurei a criar a minha própria solução: conheça include-media.
Comecei com esta lista básica de requisitos:
- Declaração dinâmica de pontos de interrupção: Eu não queria ter nenhum ponto de interrupção codificado em meu mixin. Mesmo que eu possa usar phone, tablet e desktop na maioria dos meus sites, talvez eu precise adicionar phablet, small-tablet ou huge-desktop à minha lista, dependendo do projeto. Como alternativa, talvez eu queira adotar uma abordagem diferente e fazer com que o conteúdo conduza os pontos de interrupção, de modo que eu possa ter bp-small, bp-medium, bp-wide, e assim por diante. Para resolver tudo isso adequadamente, eu queria uma maneira simples de declarar os pontos de interrupção de que preciso em algum lugar do código (de preferência fora do arquivo mixin) e, em seguida, fazer referência a seus nomes para construir as consultas de mídia.
- Sintaxe simples e natural: Minha sintaxe foi inspirada no A técnica de Dmitry Sheikoque usa um mixin que recebe como argumentos os nomes dos pontos de interrupção precedidos por um sinal de maior ou menor que (por exemplo, o
@include media(">minWidth")
) para indicar se é ummin-width
oumax-width
. - Combine pontos de interrupção com valores personalizados: Muitas vezes, eu precisava aplicar regras adicionais a um elemento em pontos de interrupção intermediários. Em vez de poluir minha lista global de pontos de interrupção com valores específicos do caso, eu queria poder lançar valores personalizados no mixin e combiná-los com meus pontos de interrupção (por exemplo, o
@include media(">tablet", "<1280px")
). - Pontos de interrupção inclusivos e exclusivos: Na maioria das soluções que tentei, quando o senhor faz do mesmo ponto de interrupção o limite superior de uma consulta de mídia e o limite inferior de outra, ambas as consultas de mídia serão acionadas quando a largura da janela de visualização for exatamente esse ponto de interrupção, porque todos os intervalos são inclusivos. Às vezes, isso pode levar a um comportamento inesperado, portanto, eu queria ter um controle mais preciso sobre essas situações com dois operadores adicionais: maior que ou igual a e menor que ou igual a.
- Uma maneira elegante de incluir tipos de mídia e expressões complexas: Eu queria poder especificar tipos de mídia, como tela ou portátilmas como um argumento opcional porque, na maioria das vezes, eu simplesmente o omitirei. Além disso, eu queria suporte adequado para condições que contêm um
or
(representado por uma vírgula no CSS), como Declaração de retina de Chris. Praticamente todos os mixins que tentei não conseguem lidar adequadamente com isso, porque quando o usuário combina as expressõesa
comb or c
eles geram(a b) or c
em vez de(a b) or (a c)
.
Então, basicamente, eu queria algo assim:
// We'll define the breakpoints somehow. // For now, let's say tablet' is 768px and 'desktop' 1024px. @include media(">=tablet", "<1280px") { } @include media("screen", ">tablet") { } @include media(">tablet", "retina2x") { } // Compiling to: @media (min-width: 768px) and (max-width: 1279px) { } @media screen and (min-width: 769px) { } @media (min-width: 769px) and (-webkit-min-device-pixel-ratio: 2), (min-width: 769px) and (min-resolution: 192dpi) { }
Vamos a isso, então.
Analisando expressões
A primeira etapa é criar uma estrutura na qual possamos definir nossos pontos de interrupção e nossos tipos e expressões de mídia. Sass 3.3 adicionado suporte a mapasque são basicamente listas multidimensionais em que os valores podem ser associados de forma semelhante ao JSON. Essa é a estrutura perfeita para acomodar nossos pontos de interrupção.
$breakpoints: (phone: 320px, tablet: 768px, desktop: 1024px) !default;
Isso é flexível o suficiente para armazenar qualquer número de pontos de interrupção que desejarmos, e nós os rotulamos automaticamente para facilitar o uso. Como provavelmente ajustaremos esses pontos de interrupção com base no projeto, nós o declaramos como !default
no mixin e, em seguida, substituímos a declaração sempre que necessário. 13;
Da mesma forma, podemos usar essa estrutura para armazenar nossos tipos de mídia e expressões.
$media-expressions: (screen: "screen", print: "print", handheld: "handheld", retina2x: ("(-webkit-min-device-pixel-ratio: 2)", "(min-resolution: 192dpi)"), retina3x: ("(-webkit-min-device-pixel-ratio: 3)", "(min-resolution: 350dpi)") ) !default;
Observe que declarei expressões que contêm disjunções lógicas como listas aninhadas, porque teremos de lidar com suas condições individualmente. Ainda poderíamos declará-las como cadeias de caracteres separadas por vírgula, como são naturalmente escritas no CSS, e depois separá-las, mas é melhor dividi-las no início.
A próxima etapa é analisar as strings que recebemos como argumentos e traduzi-las para as expressões corretas. Em vez de tentar escrever um mixin que faça tudo isso de uma vez, vamos optar por uma função menor que lide com um único argumento (por exemplo, o >=tablet em min-width: 768px).
@function parse-expression($expression) { $operator: ""; $value: ""; $element: ""; $result: ""; $is-width: true; // Separating the operator from the rest of the expression @if (str-slice($expression, 2, 2) == "=") { $operator: str-slice($expression, 1, 2); $value: str-slice($expression, 3); } @else { $operator: str-slice($expression, 1, 1); $value: str-slice($expression, 2); } // Checking what type of expression we're dealing with @if map-has-key($breakpoints, $value) { $result: map-get($breakpoints, $value); } @else if map-has-key($media-expressions, $expression) { $result: map-get($media-expressions, $expression); $is-width: false; } @else { $result: to-number($value); } // If we're dealing with a width (breakpoint or custom value), // we form the expression taking into account the operator. @if ($is-width) { @if ($operator == ">") { $element: "(min-width: #{$result + 1})"; } @else if ($operator == "<") { $element: "(max-width: #{$result - 1})"; } @else if ($operator == ">=") { $element: "(min-width: #{$result})"; } @else if ($operator == "<=") { $element: "(max-width: #{$result})"; } } @else { $element: $result; } @return $element; }
Começamos detectando qual operador foi usado e, em seguida, comparamos o restante da string com o mapa de pontos de interrupção. Se ela corresponder a uma das chaves, usaremos seu valor. Caso contrário, repetimos o processo para o mapa de expressões de mídia. Por fim, se nenhuma correspondência for encontrada, assumimos que se trata de um valor personalizado e, nesse caso, temos de converter a cadeia de caracteres em um número que podemos usar para adicionar ou subtrair conforme necessário, dependendo do operador. Eu usei to-number
do SassyCast para fazer isso.
Manipulação da disjunção lógica
Devido à forma como o or
funciona no CSS, a combinação de condições que contêm esse operador com outros pode ser bastante complicada. Por exemplo, a expressão a (b or c) d e (f or g)
gerará 4 ramificações de condições disjuntas.
Aqui está um processo que podemos usar para gerar todas essas combinações possíveis:
- Tire um “instantâneo” da expressão, deixando os singletons intocados e pegando apenas o primeiro elemento de cada
or
grupo (um “instantâneo” de uma expressão sem grupos é a expressão original). - Encontre a próxima disjunção (grupo). Pegue as expressões já calculadas (resultado) e faça (N-1) cópias, em que N é o número de elementos no grupo.
- Substitua todas as instâncias do primeiro elemento do grupo por todos os outros elementos do grupo e atualize o resultado.
- Repita a etapa 2 até que não haja mais grupos.
A tabela a seguir ilustra o processo para a expressão mostrada acima.
Iteração | Grupo | Resultado |
---|---|---|
1 | – | a b d e f (instantâneo) |
2 | (b, c) | a b d e f, a c d e f |
3 | (f, g) | a b d e f, a c d e f, a b d e g, a c d e g |
Veja como podemos escrever isso em Sass:
@function get-query-branches($expressions) { $result: ""; $has-groups: false; // Getting initial snapshot and looking for groups @each $expression in $expressions { @if (str-length($result) != 0) { $result: $result + " and "; } @if (type-of($expression) == "string") { $result: $result + $expression; } @else if (type-of($expression) == "list") { $result: $result + nth($expression, 1); $has-groups: true; } } // If we have groups, we have to create all possible combinations @if $has-groups { @each $expression in $expressions { @if (type-of($expression) == "list") { $first: nth($expression, 1); @each $member in $expression { @if ($member != $first) { @each $partial in $result { $result: join($result, str-replace-first($first, $member, $partial)); } } } } } } @return $result; }
O Sass não tem nenhum substituição de string por isso criei uma versão simples que substitui apenas a primeira ocorrência da string a ser substituída, que é o que precisamos:
@function str-replace-first($search, $replace, $subject) { $search-start: str-index($subject, $search); @if $search-start == null { @return $subject; } $result: str-slice($subject, 0, $search-start - 1); $result: $result + $replace; $result: $result + str-slice($subject, $search-start + str-length($search)); @return $result; }
Colando tudo junto
Agora que criamos nossos pequenos auxiliares, podemos finalmente escrever o mixin em si. Ele terá de pegar um número variável de cadeias de caracteres, analisá-las, detectar expressões de mídia e lidar com possíveis disjunções e colar tudo para formar a expressão de consulta de mídia.
@mixin media($conditions...) { @for $i from 1 through length($conditions) { $conditions: set-nth($conditions, $i, parse-expression(nth($conditions, $i))); } $branches: get-query-branches($conditions); $query: ""; @each $branch in $branches { @if (str-length($query) != 0) { $query: $query + ", "; } $query: $query + $branch; } @media #{$query} { @content; } }
Concluindo
Essa implementação é um pouco complexa e algumas pessoas podem argumentar que é código demais para escrever consultas de mídia. Pessoalmente, acho que essa é uma maneira fácil e confortável de escrever consultas de mídia poderosas e de fácil manutenção com uma sintaxe simples e confortável, apenas baixando e importando um único arquivo SCSS.
O repositório do GitHub está lá para que as pessoas possam enviar problemas e solicitações pull e, com isso, esperamos manter o mixin atualizado com novos recursos e melhorias. O que o senhor acha?

Sobre Eduardo Bouças
Eduardo é um desenvolvedor web português que vive em Londres e trabalha como Lead Developer para a Time Inc. UK. UK. Ele é apaixonado pela Web, por design limpo, código elegante e soluções robustas.