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.
Låt säga att man har en <Paragraph />
styled-component som ser ut ungefär så här:
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:
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.
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”.
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:
// 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;
}
';
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.
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.
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:
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.
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.
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:
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:
getBreakpointScaleFromTheme
tar som parameter en breakpoint, som i det här fallet är small, medium, eller large.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.getBreakpointScaleFromTheme
ser alltså till att läsa av ThemeContext och hämta ut efterfrågad breakpoint-värde.breakpointToMediaQuery
är den funktion som bygger upp själva media queryn. Även denna funktion tar en breakpoint som parameter.
css(templateStrings)
funktionsanrop - inuti själva media queryn.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.
Vi återbesöker Paragraf-komponenten en sista gång och nu kan den istället se ut så här:
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.
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:
🙋🏼♂️ 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å!