Presets úteis de @custom-media
Com @custom-media, uma media query ganha um nome e passa a ser reutilizada como se fosse uma variável. A seguir, presets prontos para detectar o tipo de entrada do dispositivo, montar breakpoints e reconhecer telas HDR, além de um plano B com style queries que já funciona enquanto o suporte não chega.
Em fevereiro de 2026, o Firefox 148 lançou a at-rule @custom-media (por enquanto, é preciso ativar a flag layout.css.custom-media.enabled), que serve para criar media queries personalizadas.
Como dá para imaginar, as custom media queries são como as custom properties (ou “variáveis CSS”), só que para media queries. Elas permitem reaproveitar uma media query pelo nome que demos a ela.
Alguns exemplos para dar uma ideia de como funcionam:
/* Definição básica */@custom-media --no-hover (hover: none);@custom-media --landscape (orientation: landscape);
/* Definição combinada */@custom-media --no-hover-landscape (hover: none) and (orientation: landscape);@custom-media --no-hover-landscape (--no-hover) and (--landscape);@custom-media --no-hover-landscape (hover: none) and (--landscape);
/* Uso básico */@media (--no-hover) { /* ... */ }@media (--landscape) { /* ... */ }@media (--no-hover-landscape) { /* ... */ }
/* Uso combinado */@media (--no-hover) and (--landscape) { /* ... */ }@media (--no-hover) and (orientation: landscape) { /* ... */ }@media (hover: none) and (--landscape) { /* ... */ }Os tipos de mídia (como all, screen e print), os operadores lógicos (not, and, only e a sintaxe de “ou” separada por vírgulas) e todas as outras sintaxes de media query podem ser usados com custom media queries.
Este artigo reúne alguns presets úteis de @custom-media que podem entrar em qualquer projeto sem impor muitas opiniões. Antes, porém, é preciso entender o suporte nos navegadores e as opções que existem hoje.
Suporte nos navegadores
- O Firefox tem uma flag:
layout.css.custom-media.enabled - O Chrome tem um Intent to Prototype, mas nada andou desde então.
- O Safari ainda não tem nada.
Ou seja, ainda é cedo.
Como @custom-media ainda não funciona em todos os principais navegadores, os presets também aparecem aqui numa sintaxe alternativa que já tem suporte: a das container style queries.
A sintaxe também pode ser “processada”: existe um plugin do PostCSS que dá conta do recado para quem quer se preparar para o futuro.
Queries @custom-media para capacidades do dispositivo
O tamanho da tela não indica mais a capacidade do dispositivo, se é que algum dia indicou.
Claro, antigamente bastavam três breakpoints para saber se o aparelho era um celular, um tablet ou um desktop. Hoje, nem dá para ter certeza do que esses termos significam: alguns “celulares” e “tablets” aceitam mouse e teclado, enquanto alguns “desktops” têm tela de toque e até suporte a caneta.
Breakpoints continuam úteis, mas não dizem nada sobre as capacidades do dispositivo.
O que precisamos descobrir é o mecanismo de entrada principal, para atender ao que o usuário provavelmente está usando. Isso exige uma combinação de media queries (as mesmas que servem para detectar touch com CSS puro), e @custom-media é perfeita para isso:
/* Mecanismo de entrada principal é mouse ou algo que emula mouse */@custom-media --mouse (hover: hover) and (pointer: fine);
/* Mecanismo principal é toque */@custom-media --touch (hover: none) and (pointer: coarse);
/* Caneta (stylus) */@custom-media --stylus (hover: none) and (pointer: fine);
/* Air mouse, caneta aérea ou controle remoto com ponteiro */@custom-media --air (hover: hover) and (pointer: coarse);
/* Gamepad ou controle remoto com botões direcionais */@custom-media --dpad (hover: none) and (pointer: none);
Só que, para criar boas experiências em todos os casos, também é preciso levar em conta os mecanismos de entrada secundários.
Infelizmente, não existe um jeito definitivo de saber qual deles o usuário está de fato usando quando há vários disponíveis. Dois exemplos:
- Um mouse conectado de propósito a um tablet (o usuário provavelmente está usando o mouse secundário, mas não há como confirmar)
- Um desktop com tela de toque (o usuário provavelmente usa o mouse principal, mas os dois vêm embutidos, então vai saber?)
Dá para ver como isso pode complicar, certo? Mesmo usando a família any- de media features (por exemplo, any-hover), elas dizem se algum dos mecanismos de entrada tem aquela capacidade, e não se o mecanismo secundário em particular a tem.
Mas isso resolve, não? Não. Veja isto:
@custom-media --mouse (any-hover: hover) and (any-pointer: fine);Isso não pergunta se algum dos mecanismos de entrada é um mouse. Pergunta se um mecanismo de entrada é mouse ou ponteiro aéreo (com any-hover: hover) e se um mecanismo (talvez o mesmo) é mouse ou caneta (com any-pointer: fine).
A combinação a seguir não é muito melhor. Ela significa que o mecanismo de entrada principal é mouse ou ponteiro aéreo, enquanto algum mecanismo é mouse ou caneta.
@custom-media --mouse (hover: hover) and (any-pointer: fine);O único jeito de ter certeza é ser ultraespecífico, mas será que queremos administrar um caminhão de custom media queries? Provavelmente não. Por isso, é melhor focar no mecanismo de entrada principal e garantir que o site funcione razoavelmente bem com os outros mecanismos desde o início.
Isso não significa que casos extremos não vão aparecer, apenas que pode valer a pena tratar as media queries any- como complementos. Assim dá para atingir mecanismos de entrada secundários quando necessário, sem criar custom media queries demais:
@media (--touch) and (any-hover: hover) { /* Dispositivo de toque + capacidade de hover (por exemplo, iPad com mouse) */}
@media (--touch) and (any-pointer: fine) { /* Dispositivo de toque + ponteiro preciso (por exemplo, iPad com mouse ou caneta) */}
@media (--mouse) and (any-pointer: coarse) { /* Dispositivo com mouse + capacidade de toque (por exemplo, Microsoft Surface) */}
@media (--stylus) and (any-pointer: coarse) { /* Dispositivo com caneta + capacidade de toque (por exemplo, mesa digitalizadora) */}Lembre-se: as custom media queries ainda não funcionam em todos os navegadores. Por isso, aqui estão os mesmos presets na sintaxe de container style queries, que funciona em todos eles:
/* Definição */:root { @media (hover: hover) and (pointer: fine) { --mouse: any-valid-ident; }
@media (hover: none) and (pointer: coarse) { --touch: true; }
@media (hover: none) and (pointer: fine) { --stylus: true; }
@media (hover: hover) and (pointer: coarse) { --air: true; }
@media (hover: none) and (pointer: none) { --dpad: true; }}
/* Uso */@container style(--mouse) { /* ... */ }@container style(--touch) { /* ... */ }@container style(--stylus) { /* ... */ }@container style(--air) { /* ... */ }@container style(--dpad) { /* ... */ }Uma pequena ressalva: container style queries não servem para estilizar o :root, porque ele é o elemento de nível mais alto e não tem nenhum container acima dele.
Uma ressalva menor ainda: container queries não podem ser combinadas com media queries numa mesma condição. Seria preciso transformar todas elas em custom properties, assim:
:root { @media (hover: none) and (pointer: coarse) { --touch: true; }
@media (any-hover: hover) { --any-hover-hover: true; }}
@container style(--touch) and style(--any-hover-hover) { /* Dispositivo de toque + capacidade de hover (por exemplo, iPad com mouse) */}Essa saída não agrada nem um pouco. De novo, são custom properties demais para administrar, ainda mais quando dá simplesmente para aninhar:
@container style(--touch) { @media (any-hover: hover) { /* Dispositivo de toque + capacidade de hover (por exemplo, iPad com mouse) */ }}
Queries @custom-media para breakpoints
Os tamanhos de tela variam bastante hoje em dia, então, de novo, não é tão simples quanto ter breakpoints para celular, tablet e desktop. A estratégia sugerida é “maior ou igual a [este] tamanho”, em incrementos de 100px.
Tomando estas estatísticas de resolução de tela como guia, a faixa atual vai de 360px a 1560px. As custom media queries para isso ficam assim (com a range syntax das media queries, que troca min-width por width >=):
/* Definição */@custom-media --from-360 (width >= 360px);@custom-media --from-460 (width >= 460px);@custom-media --from-560 (width >= 560px);@custom-media --from-660 (width >= 660px);@custom-media --from-760 (width >= 760px);@custom-media --from-860 (width >= 860px);@custom-media --from-960 (width >= 960px);@custom-media --from-1060 (width >= 1060px);@custom-media --from-1160 (width >= 1160px);@custom-media --from-1260 (width >= 1260px);@custom-media --from-1360 (width >= 1360px);@custom-media --from-1460 (width >= 1460px);@custom-media --from-1560 (width >= 1560px);
/* Uso */@media (--from-360) { /* ... */ }@media (--from-460) { /* ... */ }@media (--from-560) { /* ... */ }@media (--from-660) { /* ... */ }@media (--from-760) { /* ... */ }@media (--from-860) { /* ... */ }@media (--from-960) { /* ... */ }@media (--from-1060) { /* ... */ }@media (--from-1160) { /* ... */ }@media (--from-1260) { /* ... */ }@media (--from-1360) { /* ... */ }@media (--from-1460) { /* ... */ }@media (--from-1560) { /* ... */ }Aqui estão eles na sintaxe de container style queries:
/* Definição */:root { @media (width >= 360px) { --from-360: true; }
@media (width >= 460px) { --from-460: true; }
@media (width >= 560px) { --from-560: true; }
@media (width >= 660px) { --from-660: true; }
@media (width >= 760px) { --from-760: true; }
@media (width >= 860px) { --from-860: true; }
@media (width >= 960px) { --from-960: true; }
@media (width >= 1060px) { --from-1060: true; }
@media (width >= 1160px) { --from-1160: true; }
@media (width >= 1260px) { --from-1260: true; }
@media (width >= 1360px) { --from-1360: true; }
@media (width >= 1460px) { --from-1460: true; }
@media (width >= 1560px) { --from-1560: true; }}
/* Uso */@container style(--from-360) { /* ... */ }@container style(--from-460) { /* ... */ }@container style(--from-560) { /* ... */ }@container style(--from-660) { /* ... */ }@container style(--from-760) { /* ... */ }@container style(--from-860) { /* ... */ }@container style(--from-960) { /* ... */ }@container style(--from-1060) { /* ... */ }@container style(--from-1160) { /* ... */ }@container style(--from-1260) { /* ... */ }@container style(--from-1360) { /* ... */ }@container style(--from-1460) { /* ... */ }@container style(--from-1560) { /* ... */ }Breakpoints do tipo “menor que” raramente são necessários nessa estratégia, mas são fáceis de usar junto com os de “maior que” quando preciso:
/* Definição */@custom-media --from-360 (width >= 360px);@custom-media --to-659 (width <= 659px);
/* Uso (flexível) */@media (--from-360) { /* Maior ou igual a */ }@media (--to-659) { /* Menor ou igual a */ }@media (--from-360) and (--to-659) { /* Entre os dois */ }Para encurtar, também dá para expressar breakpoints intermediários numa única condição de media query, mas ao custo da flexibilidade. Imagine quantas variações disto seriam necessárias:
/* Definição */@custom-media --from-360-to-659 (360px <= width <= 659px);
/* Uso (rígido) */@media (--from-360-to-659) { /* ... */ }Esta seção é um pouco mais subjetiva que as outras, então fique à vontade para incluir height, orientation ou outra coisa, se usar com frequência suficiente para justificar mais custom media queries.
Talvez também valha adotar uma convenção de nomes (por exemplo, --xsm/--sm/--md/--lg/--xlg/etc.), mas essa escolha fica com você.
Uma query @custom-media para detectar HDR de verdade
HDR de verdade exige uma tela com alta faixa dinâmica e gama de cores ampla. Sem isso, imagens e vídeos HDR podem ficar bem feios. O resultado é brilho alto com cores estranhamente dessaturadas, cores vibrantes estranhamente escuras, ou as duas coisas.
Então, para saber se dá ou não para usar imagens e vídeos HDR de verdade, deixe esta custom media query à mão:
/* Definição */@custom-media --true-hdr (dynamic-range: high) and (color-gamut: p3);
/* Uso */@media (--true-hdr) { /* ... */ }E aqui está ela na sintaxe de container style queries:
/* Definição */:root { @media (dynamic-range: high) and (color-gamut: p3) { --true-hdr: true; }}
/* Uso */@container style(--true-hdr) { /* ... */ }No fim das contas
A expectativa é grande para usar custom media queries, mas, enquanto isso, as container style queries servem bem como alternativa.
Uma grande vantagem de brincar com elas desde já é perceber que não são tão diferentes das custom properties quando se trata de equilibrar concisão e flexibilidade. Leva um tempo para descobrir como organizá-las bem.
Quando todos os navegadores suportarem @custom-media, o autor, Daniel Schwarz, pretende colocar estes presets no seu CSS reset. Ele também quer saber se mais alguém tem ideias, ou mesmo opiniões sobre como @custom-media funciona. O Open Props tem os seus próprios presets. É uma boa ideia? Você vai usar?