Ich hatte kürzlich eine einzigartige Anforderung: ein Layout mit Full-Bleed-Elementen zu erstellen, während ein Element oben haften bleibt. Das war ziemlich knifflig zu bewerkstelligen, deshalb dokumentiere ich es hier, falls jemand denselben Effekt nachbilden muss. Ein Teil der Schwierigkeit bestand auch darin, die logische Positionierung auf kleinen Bildschirmen zu berücksichtigen.
Es ist schwierig, den Effekt zu beschreiben, also habe ich meinen Bildschirm aufgezeichnet, um zu zeigen, was ich meine. Achten Sie besonders auf den Haupt-Call-to-Action-Bereich, den mit der Überschrift „Try Domino Today“.
Die Idee ist, den Haupt-Call-to-Action auf der rechten Seite anzuzeigen, während Benutzer auf größeren Ansichten an anderen Abschnitten vorbeiscrollen. Auf kleineren Ansichten muss das Call-to-Action-Element nach dem Haupt-Hero-Abschnitt mit der Überschrift „Start your trial“ angezeigt werden.
Hier gibt es zwei Hauptprobleme
- Full-Bleed-Elemente erstellen, die das Sticky-Element nicht beeinträchtigen
- Die Duplizierung des HTML vermeiden
Bevor wir uns ein paar mögliche Lösungen (und deren Einschränkungen) ansehen, richten wir zunächst die semantische HTML-Struktur ein.
Das HTML
Beim Erstellen solcher Layouts könnte man versucht sein, *duplizierte* Call-to-Action-Bereiche zu erstellen: einen für die Desktop-Version und einen für die mobile Version, und dann deren Sichtbarkeit entsprechend zu steuern. Dies vermeidet die Notwendigkeit, die perfekte Stelle im HTML zu finden und CSS anzuwenden, das beiden Layoutanforderungen gerecht wird. Ich muss gestehen, dass ich das auch ab und zu getan habe. Aber diesmal wollte ich mein HTML nicht duplizieren.
Die andere Sache, die zu beachten ist, ist, dass wir die Sticky-Positionierung für das Element .box--sticky verwenden, was bedeutet, dass es ein Geschwisterelement anderer Elemente sein muss, einschließlich Full-Bleed-Elementen, damit es ordnungsgemäß funktioniert.
Hier ist die Markierung
<div class="grid">
<div class="box box--hero">Hero Box</div>
<div class="box box--sticky">Sticky Box</div>
<div class="box box--bleed">Full-bleed Box</div>
<div class="box box--bleed">Full-bleed Box</div>
<!-- a bunch more of these -->
</div>
Legen wir los mit Sticky
Das Erstellen von Sticky-Elementen in einem CSS-Grid-Layout ist ziemlich einfach. Wir fügen position: sticky zum Element .box--sticky mit einem Offset von top: 0 hinzu, der angibt, wo es zu haften beginnt. Ach, und beachten Sie, dass wir das Element nur für Ansichten größer als 768 Pixel zu einem Sticky-Element machen.
@media screen and (min-width: 768px) {
.box--sticky {
position: sticky;
top: 0;
}
}
Beachten Sie, dass es ein bekanntes Problem mit der Sticky-Positionierung in Safari gibt, wenn sie mit overflow: auto verwendet wird. Dies ist bei caniuse im Abschnitt „Bekannte Probleme“ dokumentiert
Ein übergeordnetes Element mit
overflowaufautogesetzt, verhindert, dassposition: stickyin Safari funktioniert.
Schön, das war einfach. Lassen Sie uns als Nächstes die Herausforderung der Full-Bleed-Elemente lösen.
Lösung 1: Pseudo-Elemente
Die erste Lösung ist etwas, das ich oft verwende: absolut positionierte Pseudo-Elemente, die sich von einer Seite zur anderen erstrecken. Der Trick hier ist die Verwendung eines negativen Offsets.
Wenn wir über zentrierte Inhalte sprechen, ist die Berechnung ziemlich einfach
.box--bleed {
max-width: 600px;
margin-right: auto;
margin-left: auto;
padding: 20px;
position: relative;
}
.box--bleed::before {
content: "";
background-color: dodgerblue;
position: absolute;
top: 0;
bottom: 0;
right: calc((100vw - 100%) / -2);
left: calc((100vw - 100%) / -2);
}
Kurz gesagt, der negative Offset ist die Breite des Viewports, 100vw, minus die Breite des Elements, 100%, und dann geteilt durch -2, da wir zwei negative Offsets benötigen.
Beachten Sie, dass es einen bekannten Fehler bei der Verwendung von 100vw gibt, der ebenfalls bei caniuse dokumentiert ist
Derzeit betrachten alle Browser außer Firefox 100vw fälschlicherweise als die gesamte Seitenbreite, einschließlich der vertikalen Scrollleiste, was zu einer horizontalen Scrollleiste führen kann, wenn
overflow: autogesetzt ist.
Nun wollen wir Full-Bleed-Elemente erstellen, wenn der Inhalt *nicht* zentriert ist. Wenn Sie sich das Video noch einmal ansehen, bemerken Sie, dass sich unter dem Sticky-Element kein Inhalt befindet. Wir möchten nicht, dass unser Sticky-Element den Inhalt überlappt, und das ist der Grund, warum wir in diesem speziellen Layout keinen zentrierten Inhalt haben.
Zuerst erstellen wir das Grid
.grid {
display: grid;
grid-gap: var(--gap);
grid-template-columns: var(--cols);
max-width: var(--max-width);
margin-left: auto;
margin-right: auto;
}
Wir verwenden benutzerdefinierte Eigenschaften, die es uns ermöglichen, die maximale Breite, den Abstand und die Grid-Spalten neu zu definieren, ohne die Eigenschaften neu zu deklarieren. Mit anderen Worten, anstatt die Eigenschaften grid-gap, grid-template-columns und max-width neu zu deklarieren, deklarieren wir die Variablenwerte neu
:root {
--gap: 20px;
--cols: 1fr;
--max-width: calc(100% - 2 * var(--gap));
}
@media screen and (min-width: 768px) {
:root {
--max-width: 600px;
--aside-width: 200px;
--cols: 1fr var(--aside-width);
}
}
@media screen and (min-width: 980px) {
:root {
--max-width: 900px;
--aside-width: 300px;
}
}
Für Ansichten ab 768 Pixel Breite haben wir zwei Spalten definiert: eine mit fester Breite, --aside-width, und eine, die den verbleibenden Platz ausfüllt, 1fr, sowie die maximale Breite des Grid-Containers, --max-width.
Für Ansichten kleiner als 768 Pixel haben wir eine einzelne Spalte und den Abstand definiert. Die maximale Breite des Grid-Containers beträgt 100 % des Viewports, abzüglich der Abstände auf jeder Seite.
Nun kommt der interessante Teil. Der Inhalt ist auf größeren Ansichten nicht zentriert, daher ist die Berechnung nicht so einfach, wie man vielleicht denkt. So sieht es aus
.box--bleed {
position: relative;
z-index: 0;
}
.box--bleed::before {
content: "";
display: block;
position: absolute;
top: 0;
bottom: 0;
left: calc((100vw - (100% + var(--gap) + var(--aside-width))) / -2);
right: calc(((100vw - (100% - var(--gap) + var(--aside-width))) / -2) - (var(--aside-width)));
z-index: -1;
}
Anstatt 100 % der Breite des übergeordneten Elements zu verwenden, berücksichtigen wir die Breiten des Abstands und des Sticky-Elements. Das bedeutet, dass die Breite des Inhalts in Full-Bleed-Elementen nicht die Grenzen des Hero-Elements überschreitet. So stellen wir sicher, dass das Sticky-Element keine wichtigen Informationen überlappt.
Der linke Offset ist einfacher, da wir nur die Breite des Elements (100 %), den Abstand (--gap) und das Sticky-Element (--aside-width) von der Viewport-Breite (100vw) abziehen müssen.
left: (100vw - (100% + var(--gap) + var(--aside-width))) / -2);
Der rechte Offset ist komplizierter, da wir die Breite des Sticky-Elements zu der vorherigen Berechnung, --aside-width, sowie den Abstand, --gap, hinzufügen müssen
right: ((100vw - (100% + var(--gap) + var(--aside-width))) / -2) - (var(--aside-width) + var(--gap));
Nun sind wir sicher, dass das Sticky-Element keine Inhalte in Full-Bleed-Elementen überlappt.
Hier ist die Lösung mit einem horizontalen Bug
Und hier ist die Lösung mit einem horizontalen Bugfix
Die Lösung besteht darin, den Überlauf auf der x-Achse des Körpers zu verbergen, was im Allgemeinen ohnehin eine gute Idee sein könnte
body {
max-width: 100%;
overflow-x: hidden;
}
Dies ist eine vollkommen praktikable Lösung, und wir könnten hier aufhören. Aber wo bleibt da der Spaß? Es gibt normalerweise mehr als eine Möglichkeit, etwas zu erreichen, also betrachten wir einen anderen Ansatz.
Lösung 2: Padding-Berechnungen
Anstatt eines zentrierten Grid-Containers und von Pseudo-Elementen könnten wir denselben Effekt erzielen, indem wir unser Grid konfigurieren. Beginnen wir mit der Definition des Grids, genau wie wir es letztes Mal getan haben
.grid {
display: grid;
grid-gap: var(--gap);
grid-template-columns: var(--cols);
}
Auch hier verwenden wir benutzerdefinierte Eigenschaften, um den Abstand und die Spaltenvorlagen zu definieren
:root {
--gap: 20px;
--gutter: 1px;
--cols: var(--gutter) 1fr var(--gutter);
}
Wir zeigen drei Spalten für Ansichten kleiner als 768 Pixel. Die mittlere Spalte nimmt so viel Platz wie möglich ein, während die anderen beiden nur dazu dienen, den horizontalen Abstand zu erzwingen.
@media screen and (max-width: 767px) {
.box {
grid-column: 2 / -2;
}
}
Beachten Sie, dass alle Grid-Elemente in der mittleren Spalte platziert sind.
Bei Ansichten größer als 768 Pixel definieren wir eine Variable --max-width, die die Breite der inneren Spalten begrenzt. Wir definieren auch --aside-width, die Breite unseres Sticky-Elements. Auch hier stellen wir sicher, dass das Sticky-Element nicht über Inhalten in den Full-Bleed-Elementen positioniert wird.
:root {
--gap: 20px;
}
@media screen and (min-width: 768px) {
:root {
--max-width: 600px;
--aside-width: 200px;
--gutter: calc((100% - (var(--max-width))) / 2 - var(--gap));
--cols: var(--gutter) 1fr var(--aside-width) var(--gutter);
}
}
@media screen and (min-width: 980px) {
:root {
--max-width: 900px;
--aside-width: 300px;
}
}
Als Nächstes berechnen wir die Gutter-Breite. Die Berechnung lautet
--gutter: calc((100% - (var(--max-width))) / 2 - var(--gap));
…wobei 100 % die Viewport-Breite sind. Zuerst ziehen wir die maximale Breite der inneren Spalten von der Viewport-Breite ab. Dann teilen wir dieses Ergebnis durch 2, um die Gutter zu erstellen. Schließlich ziehen wir den Grid-Abstand ab, um die korrekte Breite der Gutter-Spalten zu erhalten.
Nun verschieben wir das Element .box--hero so, dass es in der ersten inneren Spalte des Grids beginnt
@media screen and (min-width: 768px) {
.box--hero {
grid-column-start: 2;
}
}
Dies verschiebt automatisch die Sticky-Box, sodass sie direkt nach dem Hero-Element beginnt. Wir könnten die Platzierung der Sticky-Box auch explizit definieren, so
.box--sticky {
grid-column: 3 / span 1;
}
Schließlich machen wir die Full-Bleed-Elemente, indem wir grid-column auf 1 / -1 setzen. Das bedeutet, dass die Elemente den Inhalt beim ersten Grid-Element beginnen und bis zum letzten durchlaufen.
@media screen and (min-width: 768px) {
.box--bleed {
grid-column: 1 / -1;
}
}
Um den Inhalt zu zentrieren, berechnen wir das linke und rechte Padding. Das linke Padding entspricht der Größe der Gutter-Spalte plus dem Grid-Abstand. Das rechte Padding entspricht der Größe des linken Paddings plus einem weiteren Grid-Abstand sowie der Breite des Sticky-Elements.
@media screen and (min-width: 768px) {
.box--bleed {
padding-left: calc(var(--gutter) + var(--gap));
padding-right: calc(var(--gutter) + var(--gap) + var(--gap) + var(--aside-width));
}
}
Hier ist die endgültige Lösung
Ich bevorzuge diese Lösung gegenüber der ersten, da sie keine fehleranfälligen Viewport-Einheiten verwendet.
Ich liebe CSS-Berechnungen. Die Verwendung mathematischer Operationen ist nicht immer einfach, besonders wenn verschiedene Einheiten wie 100 % kombiniert werden. Herauszufinden, was 100 % bedeutet, ist die halbe Arbeit.
Ich liebe es auch, einfache, aber komplizierte Layouts wie dieses nur mit CSS zu lösen. Modernes CSS bietet native Lösungen – wie Grid, Sticky-Positionierung und Berechnungen –, die komplizierte und etwas schwerfällige JavaScript-Lösungen überflüssig machen. Überlassen wir die schmutzige Arbeit dem Browser!
Haben Sie eine bessere Lösung oder einen anderen Ansatz dafür? Ich würde mich freuen, davon zu hören.
Ich kann nicht glauben, dass ich das sage, aber manchmal ist es sinnvoller, JavaScript zu verwenden.
Ich verstehe dich. Aber wo bleibt da der Spaß. :D
Ich habe diese Herausforderung in Webflow angenommen, um zu sehen, was ich zustande bringe: https://sticky-fullwidth.webflow.io/
Schön! Ich habe nicht mit Webflow gearbeitet.
Übrigens, mein Sticky-Element ist auf Mobilgeräten nicht sticky, aber es könnte auch auf Mobilgeräten als sticky funktionieren.
Der CTA-Block ist nicht sticky, er ist entweder im Fluss oder in einem statischen Bereich positioniert. Ich bin neugierig, warum position: absolute dieses Problem nicht lösen würde.
Vielen Dank für Ihre Arbeit. Diese Informationen sind wirklich nützlich. Jetzt werde ich es in der Praxis ausprobieren.