Media Queries á la styled-components

2020-11-01

Häromdagen experimenterade jag lite med styled-components, som är den CSS-in-JS lösning för styling på den här sajten.

Jag testade lite olika sätt hur man kan hantera media queries på ett sånt ”styled-components-igt” sätt som möjligt. Tillslut landade jag i en lösning som kändes rätt schysst.

Med det sagt så tänkte jag gå igenom lite olika sätt man kan implementera media queries med styled-components på.

Alla exempel på media queries kommer utgå från en mobile-first approach och använda @media (min-width: pixelbredd) som exempel.

Den allra enklaste lösningen 👶🏼

Låt säga att man har en <Paragraph /> styled-component som ser ut ungefär så här:

Paragraph.js
const Paragraph = styled.p`
  color: pink;
`;

Vill man då göra den här komponenten responsiv och exempelvis få en annan färg på större skärmar skulle den kunna se ut så här istället:

Paragraph.js
const Paragraph = styled.p`
  color: pink;
  @media (min-width: 700px) {
    color: blue;
  }
`;

Komponenten skulle nu alltså resultera i en rosa paragraf på alla skärmstorlekar upp till 700px breda. Därefter skulle paragrafen vara blå. Perfekt.

Vad kan man då säga om den här lösningen?

Jo... den är väldigt enkel och straight-forward.

Nackdelen blir dock att den här komponenten själv måste hålla reda på vilken bredd som gäller för att applicera responsiva ändringar.

Den här lösningen innebär att varje komponent måste hålla reda på att det är en mobile-first approach som används i och med min-width.

Har man flera komponenter med likande media queries med just min-width: 700px hårdkodade i varje komponent måste varje komponent hållas i synk om man ändrar sin breakpoint-skala. Om man vill ändra 700px till 675px exempelvis.

Dessutom säger inte 700px ingenting egentligen. Det beskriver inte vad 700px faktiskt representerar - vilket kanske skulle kunna vara ”Alla medium-skärmar såsom tablets och uppåt”.

Den lite mer dynamiska och beskrivande lösningen 🧒🏼

Hur gör man föregående lösning lite mer dynamisk på så sätt att varje komponent inte själva behöver hålla reda på de faktiska skärmbredderna? Samtidigt som man får en media query som är lite mer beskrivande?

Jo man skulle kunna göra någonting ungefär så här:

Paragraph.js
// Låtsas att det här breakpoints objektet är en annan fil
export const breakpoints = {
  small: '550px',
  medium: '700px',
  large: '1024px',
};

// Importera breakpoints-objektet i Paragraph-komponenten
import { breakpoints } from 'somewhere/you/store/breakpoints';

const Paragraph = styled.p'
  color: pink;
  @media (min-width: ${breakpoints.medium}) {
    color: blue;
  }
';

Vad är det som är bättre med den här lösningen då?

Ovanstående exempel innebär att komponenterna inte måste hålla reda på vad breakpoint-skalan innehåller för faktiska värden.

Behöver man justera sin breakpoint-skala behöver man bara göra det på ett ställe - i breakpoints-objektet. Alla ens responsiva komponenter får då det nya pixelvärdena automatisk.

Men det kanske mest intressant med det här exemplet är hur den här lösningen bättre beskriver vad varje breakpoint faktiskt innebär. Detta för att man har grupperat breakpoint-skalan i ett objekt (eller en array för den delen) och kan namnge vad man anser att pixelvärdena innebär.

I just det här exemplet visade det sig dessutom att 700px ligger i mitten på breakpoint-skalan av small, medium och large.

Några nackdelar?

Självklart finns det några nackdelar med den här lösningen.

Varje komponent som ska vara responsiv måste importera breakpoint-skalan som man med fördel har skapat upp i ett slags theme-objekt.

Dessutom måste varje komponent fortfarande hålla koll på att det är min-width som gäller och inte max-width exempelvis.

Komponenterna måste alltså fortfarande veta vissa implementationsdetaljer kring hur media queries ska användas.

Lösningen á la styled-components 👨🏻‍🎓

Hur bygger man då egentligen vidare på föregående exempel? Hur får man en media-query-lösning som är mer i linje med hur styled-components fungerar?

Jo... egentligen skulle man kunna bryta ned det i lite olika steg:

  1. Tema i kontext: Se till att breakpoints definieras utanför ens styled-components men blir tillgängliga i komponenterna via Context - istället för att komponenterna måste importera skalan hela tiden.
  2. Media query hjälpmedel: Skapa beskrivande och återanvändbara hjälpmedel som läser av breakpoints från Context och spottar ut färdiga media queries. Använd detta hjälpmedel i komponenterna.

En stor fördel med styled-components och många andra CSS-in-JS lösningar för React är att man i sina komponenter kan läsa av ett tema från Context på väldigt smidiga sätt - som är inbyggt i styled-components.

Lägg breakpoint i React Context

Styled-components kommer med en färdig lösning för hur man hanterar temning via Context och tillhandahåller även färdiga hooks för att få tag i ThemeContext.

Vill man läsa mer om temning, Context och styled-components så går det att göra här.

Det handlar alltså om att man tar sitt färdiga tema-objekt, som innehåller det breakpoints-objekt med small, medium och large och ser till att det temat tillgängliggörs via en <ThemeProvider> komponent - högt upp i ens React-träd av komponenter.

Detta gör då alltså att alla styled-components automatiskt får tillgång till temat.

Hjälpmedel för tema-medveten media query

Vad menar jag då egentligen med ett tema-medvetet hjälpmedel för media queries?

Det är nog kanske enklast att först kika på ett kodexempel av det och sen igenom exemplet övergripande:

Media.js
const getBreakpointScaleFromTheme = (breakpoint) =>
  css(({ theme }) => theme.breakpoints[breakpoint])

const breakpointToMediaQuery = (breakpoint) => (templateStrings) => css'
	@media (min-width: ${getBreakpointScaleFromTheme(breakpoint)} {
	  css(templateStrings)
  }
'
export const media = {
	small: breakpointToMediaQuery(’small’),
	medium: breakpointToMediaQuery(’medium’),
	large: breakpointToMediaQuery(’large’)
}

Oj...Nu vart det kanske lite mycket på en gång 🧐 Let's break it down:

  1. Den minsta funktionen getBreakpointScaleFromTheme tar som parameter en breakpoint, som i det här fallet är small, medium, eller large.
  2. Den funktionen använder i sina tur css(). Vad är då det för något? Det är en hjälpfunktion som exponerar hela ThemeContext, men även eventuella props på en styled component.
  3. getBreakpointScaleFromTheme ser alltså till att läsa av ThemeContext och hämta ut efterfrågad breakpoint-värde.
  4. breakpointToMediaQuery är den funktion som bygger upp själva media queryn. Även denna funktion tar en breakpoint som parameter.
    • Den här funktionen returnerar i sin tur en funktion som tar in templateStrings som parameter och skickar den parametern till ett css(templateStrings) funktionsanrop - inuti själva media queryn.
  5. Slutligen så exporteras ett objekt som innehåller olika variationer av den templateStrings-funktion som breakpointToMediaQuery returnerar.

Vill man läsa mer om temlate strings (eller template literals) så har Wes Bos skrivit en bra genomgång av hur det hela funkar.

I det här läget kan det vara svårt att föreställa sig hur det här media-objektet egentligen ska kunna användas i Paragraf-komponenten om man inte är speciellt van med styled-components.

Hur använder man då egentligen det här media-objektet?

Vi återbesöker Paragraf-komponenten en sista gång och nu kan den istället se ut så här:

Paragraph.js
import { media } from 'where/you/put/media/helper';

const Paragraph = styled.p'
	color: pink;
	${media.medium'
		color: blue;
	'}
'

Vad hände egentligen där?

Jo... media är som bekant ett objekt med nycklar small, medium och large.

Varje nyckel representerar en funktion som tar en template string som parameter.

I bakgrunden gör funktionerna på objektet det som behövs för att läsa ut breakpoints från Context och tar hand om implementationsdetaljerna, utan att sådant läcker ut i komponenterna.

  1. media.small mappas mot det pixelvärde man har specificerat i Context på theme.breakpoints.small
  2. media.medium mappas mot det pixelvärde man har specificerat i Context på theme.breakpoints.medium
  3. media.large mappas mot det pixelvärde man har specificerat i Context på theme.breakpoints.large

Varför använda ett sånt här tillvägagångssätt?

Känns det spontant som ett komplicerat sätt att hantera media queries på? Kanske det. Men...

Tidigare har vi sett exempel på media queries i styled-components som är helt hårdkodade.

Vi har också kikat på media queries som läser av en breakpoint-skala från ett importerat objekt som är tänkt att representera ett tema.

Det här tredje exemplet må vara lite mer komplicerat men kommer, enligt mig, med en rad fördelar:

  • Komponenter behöver inte känna till implementationsdetaljer kring media queries såsom max-width eller min-width utan komponenter kan fokusera helt på vad som ska hända vid en viss breakpoint istället.
  • Komponenter är inte hårt kopplade till den konfiguration som säger vilka exakta skärmbredder som utgör en viss breakpoint. Den konfigurationen är inte intressant för komponenterna egentligen.
  • Breakpoint-skalan har till stor del abstraherats bort från komponenterna i och med att avläsning av Context sker i hjälpfunktionerna.

🙋🏼‍♂️ Personligen gillar jag det här sista sättet att jobba med media queries. För denna sajt så landade jag i en lösning väldigt lik det tredje exemplet, i alla fall i skrivande stund. Detta för att jag redan nyttjar ett tema-objekt som läses in i Context och tillgängliggörs i mina styled-components.

När jag då skriver mina komponenter behöver jag inte bry mig om implementationsdetaljer kring hur media queries struktureras i det specifika projektet.

Jag importerar bara media-objektet och slipper fokusera på hur jag ska skriva mina media queries varje gång en komponent ska ha responsivitet. Istället fokuserar jag helt på vad som ska hända vid varje breakpoint istället.

Ganska smidigt ändå!

Mer innehåll

Daniel Vernberg 2023