Zaczynamy masterowanie flexboxa, czyli poznajemy jak flexbox zajmuje się dystrybuowaniem wolnej przestrzeni, jak elementy utrzymują proporcje przy zmiennej wielkości ekranu. Po przećwiczeniu tego zostaje nam w zasadzie tylko order, marginesy i align-content, co wcale najtrudniejsze nie jest.

Właściwości dzieci – flex-grow, flex-shrink, flex-basis

Przestrzeń pozytywna:

Stwórzmy prostego flex-rodzica:

<div class="container">
        <button>btn</button>
        <button>B U T T O N</button>
</div>

Nadajmy mu style CSS – niech będzie rzędem flex-itemów:

.container {
  width: 100%;
  background-color: tomato;
  display: flex;
}

Popatrzmy na nasze elementy – ich wielkość zależy od ich contentu w wersji maksymalnej, jako że nie ustawiliśmy im żadnego width. Przestrzeń po prawej, zaznaczona kolorem tła rodzica, to przestrzeń pozytywna, o którą się elementy mogą rozszerzyć.

Max i min content:

Zanim zaczniemy rozszerzać nasze elementy o przestrzeń pozytywną zastanówmy się, co to znaczy, że wielkość elementów zależy od ich contentu w wersji maksymalnej.

Cóż, pierwszy guzik ma jeden wyraz, drugi to jednoliterowe elementy, dodajmy sobie więc trzeci, taki na dwa wyrazy, i ustawmy mu width na min-content:

<div class="container">
        <button>btn</button>
        <button>B U T T O N</button>
        <button>Hello World</button>
        <style>
            button:nth-of-type(3){
            width: min-content;
            }
        </style>
    </div>

Używamy stylów CSS zagnieżdżonych w tagach HTML, taki ciekawy feature nowego HTMLa. I mamy min-content: po hello mamy przerwę (enter, nową linię) i world już od nowej linijki.

Dzięki temu długość jest jak najmniejsza, ale wysokość się zwiększyła. Możemy to odznaczyć. Musimy tylko pamiętać, że guziki mogą zachowywać się w ten sposób.

Flex-dzieci mają domyślnie długość maksymalną dla contentu (w przypadku rzędów) a wysokość domyślnie ustawioną na stretch (czyli od góry do dołu, można zmienić w align-items).

W przypadku jednak, gdy miejsca brakuje, elementy mogą się zwijać do min-content. Guziki wielowyrazowe będą nabierać wysokości zaś tracić szerokość, jeżeli nic z tym nie zrobimy (flex-shrink 0, ale o tym za chwilę).

Warto o tym pamiętać.

Zabieramy przestrzeń pozytywną – flex-grow:

Wracamy do poprzedniego przykładu, czyli usuwamy guzik hello world i tag style zagnieżdżony tam. Teraz mamy 2 przyciski. Nie są jednakowo szerokie.

Za ich szerokość odpowiada ich content. Pierwszy ma mniejszy, drugi większy. Tylko content, nie ustawialiśmy im żadnego width, min-width, max-width ani flex-basis.

Nadal jednak możemy, po przyznaniu contentowej szerokości w niejako następnym kroku przyznać elementowi przestrzeń pozytywną, czyli wolną:

button:nth-of-type(1) {
   flex-grow: 1;
 }

Teraz nasz pierwszy guzik jest rozciągnięty niemal pod cały ekran, drugi zaś ma max-content – wiemy o tym, ponieważ jego litery (ze spacją, osobne słowa) nie zwijają się do nowej linii.

Czyli – najpierw nasze elementy dostały szerokość równą ich max-content, później pozwoliliśmy elementowi pierwszemu zagarnąć całą pozytywną przestrzeń.

Dzielimy przestrzeń pozytywną po równo – jednakowe flex-grow

Zmieńmy sobie nasz CSS:

.container {
   width: 100%;
   background-color: tomato;
   display: flex;
}
button:nth-of-type(1) {
    flex-grow: 1;
}
button:nth-of-type(2) {
    flex-grow: 1;
 }

Na szerokim ekranie wygląda to tak, jakby te guziki dostały po 1/2 ekranu, ale jak sprawdzimy w devtoolsach, to równe nie są. Po zmniejszeniu też równe nie będą.

U mnie na szerokim ekranie jeden z guzików ma ponad 60px więcej długości. Dlaczego?

W kroku pierwszym przyznawana jest elementom szerokość względem kontentu, dokładnie max-contentu. Guzik 2 miał większy max-content. Następnie, pozostała przestrzeń pozytywna została po równo podzielona na 2 elementy.

Przestrzeń wolna, pozytywna, została rozdysponowana po 1/2. Ale te elementy miały już swoją szerokość, która równa nie była, zależała od max-contentu.

Pozycjonowanie po równo – flex-basis 0

Nie jest to najlepszy, ani jedyny sposób, ale często się go używa (zwłaszcza jak poznamy shorthand property flex). Kod:

 button:nth-of-type(1) {
     flex-basis: 0;
     flex-grow: 1;
 }
button:nth-of-type(2) {
     flex-basis: 0;
     flex-grow: 1;
}
     

Flex-basis w przypadku rzędów odnosi się do szerokości (oś główna lewo/prawo czyli szerokość). Flex-basis, gdy zaaplikowane, przejmuje pałeczkę po contencie i nawet width nadpisuje.

Flex-basis na 0 oznacza, że nie ma żadnej podstawy (wcześniej podstawą była width:max-content czyli długość wynikająca z kontentu bez zwijania).

Flex grow 1 oznacza, że te elementy mając na starcie 0 po pierwsze biorą pozytywną przestrzeń (grow na wartość inną niż 0), po drugie biorą ją w jednakowej proporcji (grow w obu wypadkach 1).

Możemy uprościć sobie poprzez shorthand property ten zapis + fajny selektor wildcard child:

.container > * {
    flex: 1 1 0;
 }

Ten zapis oznacza, że wszystkie bezpośrednie dzieci (niezależnie od typu tagu) kontenera mają flex-grow na 1, flex-shrink na 1 (jeszcze nie ruszylismy, ale to domyślna wartość,) zaś flex-basis na 0.

Dzięki temu nasze guziki zajmują równo połowę, tak na wysokim jak i niskim ekranie.

Flex-basis nadpisuje width:

Wróćmy do poprzedniego przykładu:

.container {
    width: 100%;
    background-color: tomato;
    display: flex;
}        
button:nth-of-type(1) {
   /* DOMYŚLNIE
   width: max-content; */
   flex-grow: 1;
}
button:nth-of-type(2) {
  /* DOMYŚLNIE
  width: max-content; */
  flex-grow: 1;
}

W teorii wygląda, jakby nasze przyciski dostały przestrzeń po równo. W praktyce, width mają domyślnie (nie jest to do końca 100% prawda, bardziej mądrość etapu bo w przypadku braku miejsca mogą się nam przyciski załamywać) ustawione na max-content.

To znaczy – w momencie wyliczania szerokości bazowej dostają one szerokość równą max-content. Drugi ma więcej contentu i więcej dostaje o jakieś 60px.

Następnie, za pomocą flex-grow innego niż 0 i równego dostają one po połowie przestrzeń wolną, pozytywną i wygląda na to, że są prawie równe.

Co stanie się teraz?

button:nth-of-type(1) {
            /* DOMYŚLNIE
            width: max-content; */
             flex-basis: 300px;
             flex-grow: 0;
         }
         button:nth-of-type(2) {
            /* DOMYŚLNIE
            width: max-content; */
             flex-grow: 0;
          }

Teraz guzik pierwszy poprzez flex-basis nadpisuje width. Dostaje 300px na dzień-dobry. Drugi dostaje width równą swojego contentu na max ustawionego. Flex-grow na 0, a zatem przestrzeń wolna zostaje niezagospodarowana.

A teraz?

button:nth-of-type(1) {
            /* DOMYŚLNIE
            width: max-content; */
             flex-basis: 300px;
             flex-grow: 0;
         }
         button:nth-of-type(2) {
            /* DOMYŚLNIE
            width: max-content; */
             flex-basis: 300px;
             flex-grow: 0;
          }

Teraz w obu wypadkach nadpisaliśmy width. Wielkość bazowa nie zależy już od contentu tylko jest równa 300px. Guziki są równe, mają po 300px. Dodam, że flex-grow na 0 to jest domyślna wartość.

Przestawiamy na 1:

button:nth-of-type(1) {
     flex-basis: 300px;
     flex-grow: 1;
}
button:nth-of-type(2) {
      flex-basis: 300px;
      flex-grow: 1;
}

Guziki są idealnie równe i zajmują idealnie 1/2 długości rodzica. Na dzień-dobry nadpisują width, która nie zależy już od contentu, tylko wynosi 300px.

Następnie, będąc już równej długości w długości bazowej, która nie zależy już od contentu tylko wynosi 300px, rozszerzają się na przestrzeń pozytywną w równej proprocji.

Flex-basis i flex-shrink oraz basis 0 i basis absolutne:

Żeby była jasność, te kody są identycznie:

   button:nth-of-type(1) {
             /*flex-basis: 300px; */
             flex-basis: 0;
             flex-grow: 1;
 }
 button:nth-of-type(2) {
            /*flex-basis: 300px; */
            flex-basis: 0;
            flex-grow: 1;
 }

W obu przypadkach wielkość bazowa będzie identyczna i niezależna od kontentu (300px czy 0, wielkość bazowa identyczna). I tak samo, niezależnie od tego, czy bazowa to 300px czy 0, rozszerzają się równomiernie na przestrzeń pozytywną w jednakowej proporcji, czytaj mają po połowie szerokości rodzica.

Jedynie brak flex-basis (albo inna bazowa dla jednego i drugiego) sprawiłby, że width nie byłoby nadpisane i zależało od contentu, czytaj długości napisu bez enterów.

Po co zatem ustawiać bazową na 300px, skoro można na 0, ważne że oba mają jednakową bazową i jednakowe ratio flex-grow większe od 0?

Ano po to:

button:nth-of-type(1) {
             flex-basis: 300px;
             flex-grow: 1;
             flex-shrink: 0;
         }
         button:nth-of-type(2) {
            flex-basis: 300px;
            flex-grow: 1;
            flex-shrink: 0;
          }

Tutaj mamy bazową jednakową i równą 300px, niezależną od kontentu. Grow mamy na 1, czyli pozytywna przestrzeń będzie zagarniana.

Grow jest równe dla obu elementów, czyli po równo będzie pozytywna dzielona, zaś przy jednakowej bazowej oznacza to idealnie 50/50 szerokości rodzica dla naszych itemow.

Flex-shrink ustawiony na 0 sprawia zaś, że zmniejszając ekran nasze elementy będą się zmniejszać (tak, będą!) aż skończy się im to co zabrały z przestrzeni pozytywnej i już poniżej bazowej 300px nie zejdą. A jak ekran zejdzie?

Jak ekran zejdzie, to defaultowo po prostu będzie overflow-y, wyleją się poza ekran. Można temu zaradzić ustawiając wrap rodzicowi:

.container {
        width: 100%;
        background-color: tomato;
        display: flex;
        flex-wrap: wrap;
}
        
button:nth-of-type(1) {
        flex-basis: 300px;
             flex-grow: 1;
             flex-shrink: 0;
         }
         button:nth-of-type(2) {
            flex-basis: 300px;
            flex-grow: 1;
            flex-shrink: 0;
          }

Teraz sytuacja jest następująca:

  • Każdy dostał bazową 300px, czyli na start są po równo
  • Każdy dostał grow 1 jednakowe, czyli działkują się przestrzenią wolną po równo
  • W momencie zmniejszania się szerokości ekranu zmniejszają się uwalniając zagarniętą przestrzeń pozytywną
  • Jako że flex-shrink 0, w momencie osiągnięcia bazowej czyli 300px już się dalej nie zmniejszają
  • Jako że flex-wrap rodzica to wrap zamiast wylewać się na prawo (overflow-y) poza ekran spadają do następnej linii
  • Jako że teraz mamy 2 rzędy zamiast jednego proces się powtarza:
    • Bazowa: 300px
    • Flex-grow 1, zagarniam pozytywną przestrzeń
    • Jestem sam jeden w tym rzędzie, więc teraz zagarniam całość szerokości

Tylko czytając to, co piszę, zwariujemy zanim cokolwiek zrozumiemy, więc mam nadzieję, że jednocześnie to kodujemy i trenujemy.

FAQ: Mam problem! Basis i grow ustawione, shrink na 0 a dalej się zmniejsza!

Wolę wspomnieć o tym od razu. Możemy ustawić basis na jakiś procent, ładnie nabrało szerokości większej niż content, no fajnie. Teraz grow na 1, ładnie się podziałkowało wolną przestrzenią. Teraz shrink 0, okej, nie będzie malało.

Następnie zmniejszamy ekran i mamy coś niezrozumiałego. Cóż, jeżeli bazowa jest procentem a zmniejszamy ekran to ten procent się zmniejsza. I jesteśmy w takiej trochę pętli, jak uczący się podstawowych hooków Reacta czasami.

Jeżeli jednak rozumiemy jak działa bazowa, dzielenie się pozytywną przestrzenią i negatywną, to zrozumiemy szybko, że bazowa musi być absolutna. W pikselach.

Jeżeli ustawimy bazową na %, to to nie będzie żadna baza, bo zmniejszając ekran zmniejszy się procent. I zejdzie poniżej tego, co chcieliśmy, żeby nie zeszło, i myśleliśmy, że jest ok.

Zatem przeanalizujmy to, czego się nauczyliśmy raz jeszcze, pamiętając jednocześnie, że bazowa ma być abolutna albo ma być 0 albo ma jej nie być wcale, wtedy to content w wersji max jest bazową.

Tak – jak nie ma bazowej, content w wersji max jest bazową. Ale jeżeli będzie trzeba, skurczy się poniżej content-maxa dając entery między spacjami, dodając wysokość kosztem szerokości – chyba że shrink ustawimy na 0.

W przypadku braku bazowej ale shrink 0 to content jest bazową ale zmniejszać się nie będzie nasz przycisk na takiego brzydkiego taba z (na przykład) enterem między hello a world.

Zasady przydzielania miejsca – przypomnienie

Flex-basis a flex-direction rodzica:

  • W przypadku row, czyli rzędu, oś główna to lewo/prawo, zaś flex-basis określa width, którą nadpisuje
  • W przypadku column, czyli kolumny, oś główna to góra/dół zaś flex-basis określa height, którą nadpisuje

Flex-basis a jej brak:

  • Flex-basis ustawione na 0 daje 0 bazowej, ale sprawia, że bazowa nie zależy od max-contentu
  • Flex-basis ustawione na wartość absolutną, najlepiej px, nadaje bazową, której wielkość nie zależy już od max-contentu
  • Brak flex-basis sprawa, że bazowo elementy dostają szerokość dyktowaną przez max-content. Przy flex-shrink równym 0 nigdy poniżej max-contentu nie zejdą, przy flex-shrink równym 1 mogą się zapaść do min-contentu

Dzielenie się po równo:

  • Elementy muszą mieć jednakową bazową
  • Elementy muszą zagarniać pustą przestrzeń w jednakowej proporcji
button:nth-of-type(1) {
     flex-basis: 300px;
     flex-grow: 1;
}
button:nth-of-type(2) {
      flex-basis: 300px;
      flex-grow: 1;
}

Te elementy mają bazową 300px (szerokość bo to rząd jest) i jednakowe flex-grow inne niż 0, czyli zajmują przestrzeń pozytywną (wolną) w jednakowej proporcji, zatem wszystko jest idealnie po równo.

button:nth-of-type(1) {
     flex-basis: 0;
     flex-grow: 1;
}
button:nth-of-type(2) {
      flex-basis: 0;
      flex-grow: 1;
}

Te elementy mają bazową 0, czyli niezależna od kontentu i jednakowa, ale de facto nie ma bazowej, poniżej której możemy przez flex-shrink 0 zablokować zmniejszanie się.

Tym niemniej, bazowa równa i niezależna od contentu, grow zagarniające pozytywną przestrzeń w jednakowej proporcji, a zatem są podzielone po równo.

Flex-shrink 0 i 1

  • Jeżeli flex-shrink 0 bez ustawionej bazowej, to elementy nigdy nie zapadną się poniżej ich max-content (zamieniając guziki na taby, to najlepszy przykład)
  • Jeżeli flex-shrink na 0 i bazowa ustawiona to elementy nigdy nie zmniejszą się poniżej bazowej
  • Jeżeli bazowa ustawiona, flex-shrink na 0 ustawione, ale flex-grow 1 to dopóki elementy mają jakąś pozytywną przestrzeń, przez grow zagarniętą, BĘDĄ ją uwalniać przy zwężaniu ekranu – flex-shrink 0 sprawia, że poniżej bazowej nie zejdą
  • Jeżeli flex-shrink 1 to elementy mogą się skurczyć poniżej bazowej, zaś gdy takowej nie mają mogą z max-contentu skurczyć się do min-contentu
  • Jeżeli flex-shrink 0 blokuje zwężanie elementu, ale rodzic ma flex-wrap na wrap to element przeskakuje do kolejnego rzędu
  • Jeżeli flex-shrink 0 blokuje zwężanie elementu, ale rodzic nie ma flex-wrap na wrap to element wylewa się w prawo (overflow-y) zamiast się kurczyć.

Domyślne ustawienia

  • Domyślnie flex-basis nie jest ustawione. Bazową jest max-content
  • Domyślnie flex-grow jest ustawione na 0, czyli elementy (w rzędzie) nie będą się rozszerzać
  • Domyślnie flex-shrink ustawione jest na 1, czyli elementy będą nie tylko się kurczyć z pozytywnej (gdyby im flex-grow ustawić) ale i kurczyć poniżej bazowej a nawet max-contentu, jeżeli bazowej nie ma

Mamy skrót flex, który przyjmuje grow, shrink i bazową. Przykład:

.flex-parent > * {
    flex: 1 1 0;
 }

Dzieci rodzica będą się rozszerzać i zwężać zaś bazowa na 0. Rozciągną się po równo, ale i niemal zniknąć mogą.

Inny przykład:

.flex-parent > * {
            flex: 1 0 300px;
}

Tutaj mamy:

  • flex-grow 1, czyli bierz pustą przestrzeń, dziel po równo
  • flex-shrink 0 czyli z pustej przestrzeni możesz się kurczyć, ale nie poniżej bazowej
  • Bazowa 300px i równa dla wszystkich dzieci, czyli poniżej 300px się nie skurczysz, będziesz mieć 300px + po równo podzieloną pustą pozytywną

Wiem, że to straszne, sam też lekko ucząc się tego nie miałem. To jeszcze kilka zapisów na koniec.

.flex-item {
flex-basis: auto;
}

Auto bez width to jakby flex-basis nie było, content max jest bazową.

.flex-item {
width: 200px;
flex-basis: auto;
}

Width ustawiona zaś basis-auto, czyli nie nadpisuj width tylko ustaw bazową na width

.flex-item {
width: 200px;
}

Width ustawione ale brak flex-basis, czyli kontent nie będzie bazową, ale 200px (bałaganiarstwo i niechlujstwo, == flex-basis:200px).

.flex-item {
width: 200px;
flex-basis: 300px;
}

Width 200px, czyli nie kontent a 200px. Ale zaraz potem basis, które nadpisuje width, które nadpisało width: max-content (tak prawie). Czytaj – bazowa 300px, width nie ma efektu.

To się wydaje straszne, zwłaszcza gdy mamy iście tabularycznego grida, którego można logicznie rozłożyć na czynniki pierwsze i zrozumieć bez takich bardziej chemicznych procesów gdzie stężenie jednego wpływa na drugie.

Tym niemniej robimy naprawdę dużo rzeczy, które nie są gridami newsowymi, ale navbarami, kolumnami, rzeczami skaczącymi na media query od bycia kolumną do bycia wierszem lub odwrotnie i znajomość flexboxa, taka dobra znajomość, popłaca.

Warto to wszystko przestudiować, potem przećwiczyć i jeszcze raz przestudiować. A potem dużo ćwiczyć!