De meeste moderne websites gebruiken inmiddels technieken voor responsive webdesign, zodat websites er mooi, leesbaar en functioneel uitzien, onafhankelijk van de schermgrootte van de bezoeker, of die nu een beamer, laptop, smartphone, computer of televisie gebruikt.

Websites die deze technieken toepassen hebben één template die de layout van de website kan aanpassen aan de afmetingen van het scherm van elke bezoeker:

  • Kleinere schermen gebruiken meestal een langer ontwerp met één kolom, waarbij je elementen in de gebruikersinterface, zoals het menu, kan activeren door op een icoon te klikken.
  • Grotere schermen tonen meer informatie in één keer, vaak met horizontale menu-balken. UI elementen zoals menu-items zijn dan altijd direct zichtbaar.

Een belangrijk onderdeel van responsief webdesign is de implementatie van een CSS of Javascript media query om de grootte van het bezoekende apparaat te detecteren, zodat automatisch het juiste ontwerp getoond kan worden. In dit artikel leggen we uit waarom deze queries belangrijk zijn en hoe je ze kan gebruiken, maar daarvoor moeten we het eerst hebben over responsive webdesign in het algemeen.

Waarom is responsive webdesign belangrijk?

Het is onmogelijk om één vaste layout voor een pagina te bieden en dan te verwachten dat dit overal en voor iedereen werkt.

Toen mobiele telefoons voor het eerst toegang begonnen te krijgen aan het begin van het millennium, maakten websitebeheerders vaak twee of drie aparte templates die gericht waren op mobiele bezoekers. Maar naarmate mobiele apparaten exponentieel groeiden in populariteit, werd dit een onpraktische oplossing.

Vandaag de dag zijn er ontelbaar veel schermgroottes, van minuscule displays van smartwatches, tot enorme 8K monitors. Zelfs als je je beperkt tot mobiele telefoons, kunnen veel nieuwe apparaten een hogere resolutie hebben dan menig eenvoudige laptop.

Mobiel browsergebruik is ook inmiddels verder gegroeid dan desktops. Tenzij je website een specifieke doelgroep heeft die het anders doet, mag je verwachten dat de meeste mensen je website bezoeken op een smartphone. Apparaten met kleinere schermen zijn dus belangrijk, en het ontwerp moet daar dan ook op gericht zijn, zelfs al gebruiken de meeste ontwerpers, developers en hun klanten nog altijd vaker een computer voor hun werk.

Google heeft al ingezien hoe belangrijk mobiele apparaten zijn. Websites staan hoger in de zoekresultaten van Google als ze goed te gebruiken zijn op een smartphone. Goede content blijft natuurlijk ook belangrijk, maar een website die traag laadt op een mobiel en zich niet aanpast aan de schermafmetingen van de gebruikers, kan je bedrijf echt geld kosten.

Als laatste is ook toegankelijkheid erg belangrijk. Een website die voor iedereen te gebruiken is, wat voor apparaat ze ook gebruiken, zal per definitie een groter bereik hebben. Toegankelijkheid is in veel landen ook wettelijk vereist, maar zelfs als dat voor jou niet geldt, moet je bedenken dat meer bezoekers altijd meer conversies en winst biedt.

Hoe werkt responsive webdesign?

De basis van responsive webdesign ligt in media queries: een CSS technologie die een bepaalde stijl kan toepassen op basis van bepaalde indicatoren, zoals het soort output (een computerscherm, printen, of zelfs spraak), schermafmetingen, verhouding van de display, oriëntatie van het apparaat, kleurdiepte en nauwkeurigheid van de aanwijzer. Media queries kijken ook naar de voorkeuren van de gebruiker, zoals minder animaties, donkere of lichte modus, en verhoogd contrast.

De voorbeelden voor media queries laten alleen maar zien wat je kan met de breedte van het scherm, maar websites kunnen nog veel meer doen. Kijk maar eens naar de volledige set met mogelijkheden op MDN.

De ondersteuning voor media queries is uitstekend, en bestaat in de meeste browsers al meer dan tien jaar. Alleen browsers ouder dan Internet Explorer 8 bieden hier geen ondersteuning voor. Zij negeren de stijlen die media queries aanbieden, maar dat kan soms ook een voordeel zijn (lees maar eens verderop in de Best practices).

Er zijn drie gebruikelijke manieren om stijlen toe te passen op basis van media queries. De eerste optie laadt specifieke stylesheets in HTML code. De volgende tag laadt bijvoorbeeld de wide.css stylesheet wanneer een apparaat een breedte heeft boven de 800 pixels:

<link rel="stylesheet" media="screen and (min-width: 800px)" href="wide.css" />

Als tweede optie kunnen stylesheets voorwaardelijk geladen worden in CSS bestanden via een @import regel:

/* main.css */
@import url('wide.css') screen and (min-width: 800px);

Over het algemeen zal je media queries toepassen binnen stylesheets via een @media CSS at-rule block die een bepaalde stijl aanpast. Bijvoorbeeld:

/* default styles */
main {
  width: 400px;
}

/* styles applied when screen has a width of at least 800px */
@media screen and (min-width: 800px) {
  main {
    width: 760px;
  }
}

Developers kunnen een regel voor media queries toepassen die nodig zijn om de layout van een bepaalde site aan te passen.

Best practices voor media queries

Wanneer media queries voor het eerst werden ingevoerd, kozen veel websites voor een set vaste layouts. Dit is conceptueel namelijk makkelijker te ontwerpen en te programmeren, omdat je in feite gewoon een beperkt aantal templates voor pagina’s maakt. Bijvoorbeeld:

  1. Schermen met een breedte onder de 600px gebruiken een 400px brede layout, met name voor mobiel gebruik.
  2. Schermen met een breedte tussen de 600px en 999px gebruiken een 600px brede layout, voor tablets en dergelijke.
  3. Schermen boven de 1000px breed gebruiken een 1000px brede layout voor computers.

Deze techniek is niet optimaal. De resultaten op de kleinste of juist grote schermen zijn niet mooi, en als de algemene maten van apparaten en bijbehorende schermen veranderen, moet de CSS steeds bijgehouden worden.

Een betere keuze is om een mobile-first flexibel ontwerp te gebruiken met breekpunten, waardoor de layout op een bepaalde grootte aangepast wordt. In feite gebruikt de standaard layout de eenvoudigste stijlen voor kleine schermen, die elementen in lineaire verticale blokken plaatsen.

Zie bijvoorbeeld een <article> en <aside> binnen een <main> container:

/* default small-screen device */
main {
  width: 100%;
}

article, aside {
  width: 100%;
  padding: 2em;
}

Dit zijn de resultaten daarvan in alle browsers, zelfs in de oudste browsers zonder ondersteuning voor media queries:

Voorbeeld zonder ondersteuning voor media queries
Voorbeeld zonder ondersteuning voor media queries

Wanneer media queries wel ondersteund worden en het scherm zit boven een bepaalde breedte, bijvoorbeeld 500px, dan worden de elementen <article> en <aside> horizontaal gepositioneerd. Dit voorbeeld gebruikt een CSS grid, waar de primaire content ongeveer twee derde van de breedte gebruikt, en de secundaire content de overige een derde:

/* larger device */
@media (min-width: 500px) {
  main {
    display: grid;
    grid-template-columns: 2fr 1fr;
    gap: 2em;
  }

  article, aside {
    width: auto;
    padding: 0;
  }
}

Dit is dan het resultaat op grotere schermen:

Voorbeeld met ondersteuning voor media queries.
Voorbeeld met ondersteuning voor media queries.

Alternatieven voor media queries

Responsive ontwerpen kunnen ook geïmplementeerd worden in moderne CSS, met nieuwere eigenschappen die per definitie de layout aanpassen, zonder de afmetingen van de viewport mee te nemen. Opties zijn bijvoorbeeld:

  • calcmin-widthmax-widthmin-heightmax-height, en de nieuwere clamp eigenschap kunnen allemaal afmetingen definiëren waardoor elementen in grootte afgestemd worden om de beschikbare ruimte en beperkingen.
  • De viewport units vwvhvmin, en vmax kunnen de grootte van elementen aanpassen op basis van schermverhoudingen.
  • Tekst kan in CSS kolommen gezet worden, die getoond of verborgen worden op basis van de beschikbare ruimte.
  • Elementen kunnen in grootte variëren volgens de afmetingen van hun child elementen, via min-contentfit-content, en  max-content
  • CSS flexbox kan elementen wrappen, of juist niet, wanneer ze de beschikbare ruimte overschrijden.
  • CSS grid elementen kunnen aangepast worden met proportionele fr De repeat CSS functie kan samen met minmaxauto-fit, en auto-fill gebruikt worden om de beschikbare ruimte te verdelen.
  • De nieuwe, en momenteel nog experimentele, CSS container queries kunnen reageren op de gedeeltelijke ruimte die beschikbaar is op een component binnen een layout.

Deze opties gaan te ver voor dit artikel, maar ze zijn vaak wel geschikter dan eenvoudige media queries, die zich alleen maar kunnen aanpassen op de afmetingen van een scherm. Als je een layout kan bepalen zonder media queries, zal dit in de regel minder code gebruiken, efficiënter zijn, en minder onderhoud vergen.

Desalniettemin zijn er genoeg opties waarbij media queries de enige redelijke opties zijn. Ze blijven dan ook essentieel wanneer je ook andere factoren mee wil nemen, zoals aspect ratio’s, oriëntatie van apparaten, kleurdiepte, nauwkeurigheid van de aanwijzer, en gebruikersvoorkeuren zoals minder animaties of de lichte/donkere modus.

Heb je media queries nodig in JavaScript?

We hebben het tot nu toe vooral over CSS gehad. Dat komt omdat de meeste uitdagingen qua layout met uitsluitend CSS opgelost kunnen en moeten worden.

Maar er zijn ook situaties waarbij het praktischer is om een JavaScript media query te gebruiken dan een CSS versie, bijvoorbeeld:

  • Een bepaald onderdeel, zoals een menu, heeft verschillende functies op een groot en een klein scherm.
  • Omschakelen van horizontale naar verticale oriëntatie van een apparaat verandert de functionaliteit van een app.
  • Een game die aanraking gebruikt moet bijvoorbeeld de <canvas> layout wijzigen en de bediening aanpassen.
  • Een webapp volgt de voorkeuren van een gebruiker, zoals een donkere of lichte modus, minder animaties, minder nauwkeurige aanraking, etc.

De volgende onderdelen laten drie manieren zien die media queries gebruiken, of opties die daarop lijken, binnen JavaScript. Alle voorbeelden sturen een state string terug, waarbij:

  • small view = een scherm met een breedte van minder dan 400 pixels;
  • medium view = een scherm met een breedte tussen de 400 en 799 pixels’; en
  • large view = een scherm met een breedte van 800 pixels of meer.

Optie 1: Het monitoren van de afmetingen van de viewport

Dit was de enige mogelijkheid voordat media queries geïmplementeerd werden. JavaScript zou zoeken naar “resize” events van de browser, de afmetingen van de viewport analyseren via window.innerWidth en window.innerHeight of (document.body.clientWidth en document.body.clientHeight in de oude IE) en daar vervolgens mee aan de slag gaan.

Deze code geeft de berekende small, medium, of large string door naar de console:

const
  screen = {
    small: 0,
    medium: 400,
    large: 800
  };

// observe window resize
window.addEventListener('resize', resizeHandler);

// initial call
resizeHandler();

// calculate size
function resizeHandler() {

  // get window width
  const iw = window.innerWidth;
 
  // determine named size
  let size = null;
  for (let s in screen) {
    if (iw >= screen[s]) size = s;
  }

  console.log(size);
}

Je kan hier een werkende demonstratie zien. (Als je een browser op een computer gebruikt, kan je het beste een nieuw venster openen. Mobiele gebruikers kunnen het scherm draaien.)

Bovenstaande voorbeeld bekijkt de grootte van de viewport wanneer de browser een nieuwe grootte krijgt, en bepaalt vervolgens of dit small, medium of large is, en stelt dat in als class in het body element, wat vervolgens de achtergrondkleur wijzigt.

De voordelen van deze methode zijn onder meer:

  • Het werkt in elke browser die JavaScript kan gebruiken, inclusief oude toepassingen.
  • Je kan de precieze afmetingen vastleggen en die gebruiken.

En de nadelen:

  • Het is een oude techniek met veel code.
  • Is het te precies? Moet je echt per se weten dat de breedte 966 px is, en niet 967?
  • Je moet wellicht de afmetingen handmatig vergelijken met een vergelijkbare CSS media query.
  • Sommige gebruikers veranderen de afmetingen van hun browser vaker, waardoor de handler functie steeds opnieuw uitgevoerd moet worden. Zeker bij oudere en tragere browsers kan dat problemen opleveren, waardoor ze een beperking instellen. Deze functie kan dan slechts één keer per 500 milliseconden geactiveerd worden.

Kortom, de viewport afmetingen monitoren is geen goede optie, tenzij je ingewikkelde en specifieke redenen hebt om het wel te doen.

Optie 2: Een custom CSS eigenschap (variabele) bepalen en monitoren

Dit is een vrij ongebruikelijke techniek die de waarde van een custom eigenschappen string in CSS aanpast wanneer een media query geactiveerd wordt. Custom eigenschappen worden ondersteund in alle moderne browsers (maar niet in IE).

In het onderstaande voorbeeld wordt de --screen custo property ingesteld op “small”, “medium” of “large” door middel van een @media code block:

body {
  --screen: "small";
  background-color: #cff;
  text-align: center;
}

@media (min-width: 400px) {
 
  body {
    --screen: "medium";
    background-color: #fcf;
  }
 
}

@media (min-width: 800px) {
 
  body {
    --screen: "large";
    background-color: #ffc;
  }
 
}

De waarde kan in CSS geproduceerd worden door middel van een pseudo-element (maar het moet dan wel binnen enkele of dubbele aanhalingstekens staan):

p::before {
  content: var(--screen);
}

Je kan de waarde van de custom property ophalen via JavaScript:

const screen = getComputedStyle(window.body)
                 .getPropertyValue('--screen');

Maar dit is nog niet het hele verhaal, aangezien de uiteindelijke waarde ook alle witruimte en aanhalingsteken bevat die na de dubbele punt in CSS staan. De string zal dus “large” zijn, dus we moeten een beetje opruimen:

// returns small, medium, or large in a string
const screen = getComputedStyle(window.body)
                 .getPropertyValue('--screen')
                 .replace(/\W/g, '');

Je kan hier een werkende demonstratie zien. (Als je een browser op een computer gebruikt, kan je het beste een nieuw venster openen. Mobiele gebruikers kunnen het scherm draaien.)

Het voorbeeld bekijkt elke twee seconden de waarde in CSS. Het vereist een beetje JavaScript code, maar dat is nodig om de veranderingen in de gaten te houden. Je kan in CSS namelijk niet automatisch detecteren dat de waarde van de custom property veranderd is.

Het is ook niet mogelijk om de waarde naar een pseudo-element te schrijven en de verandering te detecteren via een DOM Mutation Observer. Pseudo-elementen zijn namelijk geen “echt” onderdeel van de DOM.

De voordelen:

  • Het is een eenvoudige techniek die vooral CSS gebruikt, en sterk lijkt op normale media queries.
  • Je kan ook andere CSS eigenschappen aanpassen.
  • Je hoeft geen JavaScript media query strings te kopiëren of te verwerken.

Het voornaamste nadeel is dat je niet automatisch op een wijziging van een verandering in de browser-afmetingen kan reageren. Als de gebruiker hun telefoon dus draait, zal JavaScript dat niet meteen doorhebben. Je kan (nog) vaker kijken naar veranderingen, maar dat is inefficiënt en zal in de vertraging resulteren die je in de demo ziet.

Het monitoren van CSS custom properties is een nieuwe eigenschap, maar is alleen nuttig wanneer:

  1. De layout vaststaat op het moment dat een pagina voor het eerst weergegeven wordt. Een kiosk of betaalterminal is bijvoorbeeld een mogelijkheid, maar die hebben meestal vaste afmetingen en een eenvoudige layout, dus dan zijn JavaScript media queries weer niet nodig.
  2. Een site of app gebruikt veel tijdsgebaseerde functies, zoals de animatie in een game. De custom property kan gecontroleerd worden wanneer er ook wordt gecontroleerd of de layout gewijzigd moet worden.

Optie 3: De matchMedia API gebruiken

De matchMedia API is wat ongebruikelijk, maar maakt het mogelijk om een JavaScript media query te implementeren. Het wordt ondersteund in de meeste browsers, inclusief IE vanaf versie 10. De constructor stuurt een MediaQueryList object terug met een ‘matches’ eigenschap, die True of False is voor de specifieke media query.

De volgende code komt bijvoorbeeld op True uit voor een viewport die 800px of groter is:

const mqLarge  = window.matchMedia( '(min-width: 800px)' );
console.log( mqLarge.matches );

Een “change” event kan toegepast worden op het MediaQueryList object. Dit wordt elke keer geactiveerd wanneer de staat van de ‘matches’ eigenschap verandert: het wordt True (boven 800px) nadat het eerst False (onder 800px) was, en andersom.

De ontvangende handler functie krijgt het MediaQueryList object als eerste parameter:

const mqLarge  = window.matchMedia( '(min-width: 800px)' );
mqLarge.addEventListener('change', mqHandler);

// media query handler function
function mqHandler(e) {
 
  console.log(
    e.matches ? 'large' : 'not large'
  );
 
}

De handler wordt alleen uitgevoerd wanneer de ‘matches’ eigenschap verandert. Het wordt dus niet uitgevoerd bij de eerste weergave van de pagina, dus je kan de functie direct callen om de startstatus te bepalen:

// initial state
mqHandler(mqLarge);

De API werkt goed wanneer je tussen twee specifieke states moet kiezen. Om drie of meer opties te analyseren, zoals small, medium en large, heb je meer code nodig.

Begin door een screen state object te definiëren met de bijbehorende matchMedia objecten:

const
  screen = {
    small : null,
    medium: window.matchMedia( '(min-width: 400px)' ),
    large : window.matchMedia( '(min-width: 800px)' )
  };

Het is niet nodig om een matchMedia object te definiëren voor de small state, aangezien de medium event handler geactiveerd wordt wanneer je wisselt tussen small en medium.

Event listeners kunnen vervolgens ingesteld worden voor de events medium en large. Deze callen dezelfde mqHandler() handler functie:

// media query change events
for (let [scr, mq] of Object.entries(screen)) {
  if (mq) mq.addEventListener('change', mqHandler);
}

De handler functie moet alle MediaQueryList objecten controleren om te bepalen of smallmedium, of large  momenteel actief is. Matches moeten op volgorde uitgevoerd worden, aangezien een breedte van 999px klopt bij zowel medium als large, maar alleen de grootste zou geactiveerd moeten worden.

// media query handler function
function mqHandler() {
 
  let size = null;
  for (let [scr, mq] of Object.entries(screen)) {
    if (!mq || mq.matches) size = scr;
  }
 
  console.log(size);
 
}

Je kan hier een werkende demonstratie zien. (Als je een browser op een computer gebruikt, kan je het beste een nieuw venster openen. Mobiele gebruikers kunnen het scherm draaien.)

De mogelijkheden voor dit voorbeeld zijn:

  1. Media queries in CSS die een custom property instellen en weergeven (zoals in optie 2 te zien).
  2. Identieke media queries in matchMedia objecten die veranderingen in afmetingen monitoren via JavaScript. De JavaScript output zal dan op precies hetzelfde moment veranderen.

De voordelen van de matchMedia API zijn:

  • Het is event-driven en efficiënt in het verwerken van veranderingen bij de media query.
  • Het gebruikt precies dezelfde media query strings als CSS.

En de nadelen:

  • Twee of meer media queries uitvoeren vereist een hoop denkwerk en logica in de code.
  • Je moet waarschijnlijk dubbele media queries strings maken in CSS en JavaScript. Dit kan fouten opleveren.

Om fouten tussen media queries te voorkomen, zou je design tokens in je systeem kunnen inbouwen. Media query strings worden gedefinieerd in een JSON bestand (of iets soortgelijks) en de waarden worden overgezet naar de CSS en JavaScript code tijdens het opbouwen.

Samenvattend is de matchMedia API waarschijnlijk de meest efficiënte en praktische manier om een JavaScript media query te implementeren. Je moet opletten op een paar ongebruikelijke dingen, maar het meestal de beste oplossing.

Samenvatting

Intrinsieke CSS mogelijkheden voor afmetingen zijn erg handig, maar media queries zullen voor de meeste websites de basis blijven voor hun responsive webdesign. Ze zullen altijd nodig zijn om complexere layouts en voorkeuren te verwerken, zoals een donkere en lichte modus.

Probeer media queries zoveel mogelijk tot puur CSS te beperken. Wanneer je geen keuze hebt, en wel bezig moet met JavaScript, dan biedt de matchMedia API extra controle over onderdelen van je JavaScript media query, die extra functionaliteit vereist op basis van afmetingen.

Heb je nog andere tips voor het implementeren van een JavaScript media query? Deel ze in de reacties hieronder!

Craig Buckler

Freelance UK web developer, writer, and speaker. Has been around a long time and rants about standards and performance.