Codelab: Stories-Komponente erstellen

In diesem Codelab erfährst du, wie du Instagram Stories im Web gestaltest. Wir erstellen die Komponente und beginnen mit HTML, CSS und JavaScript.

In meinem Blogpost Komponente „Stories“ erstellen erfährst du mehr über die schrittweisen Verbesserungen beim Erstellen dieser Komponente.

Einrichtung

  1. Klicke auf Zu bearbeitende Remixe, damit das Projekt bearbeitet werden kann.
  2. Öffnen Sie app/index.html.

HTML

Ich versuche immer, semantisches HTML zu verwenden. Da jeder Freund eine beliebige Anzahl von Geschichten haben kann, hielt ich es für sinnvoll, für jeden Freund ein <section>-Element und für jede Geschichte ein <article>-Element zu verwenden. Fangen wir jedoch noch einmal von vorn an. Zuerst brauchen wir einen Container für die Story-Komponente.

Fügen Sie der <body> ein <div>-Element hinzu:

<div class="stories">

</div>

Füge einige <section>-Elemente hinzu, um Freunde darzustellen:

<div class="stories">
  <section class="user"></section>
  <section class="user"></section>
  <section class="user"></section>
  <section class="user"></section>
</div>

Fügen Sie einige <article>-Elemente hinzu, um Geschichten darzustellen:

<div class="stories">
  <section class="user">
    <article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
    <article class="story" style="--bg: url(https://picsum.photos/480/841);"></article>
  </section>
  <section class="user">
    <article class="story" style="--bg: url(https://picsum.photos/481/840);"></article>
  </section>
  <section class="user">
    <article class="story" style="--bg: url(https://picsum.photos/481/841);"></article>
  </section>
  <section class="user">
    <article class="story" style="--bg: url(https://picsum.photos/482/840);"></article>
    <article class="story" style="--bg: url(https://picsum.photos/482/843);"></article>
    <article class="story" style="--bg: url(https://picsum.photos/482/844);"></article>
  </section>
</div>
  • Wir nutzen einen Bilddienst (picsum.com), um Prototypen für Stories zu erstellen.
  • Das style-Attribut für jede <article> ist Teil einer Platzhalter-Lademethode, über die wir im nächsten Abschnitt mehr erfahren.

CSS

Unsere Inhalte sind stilsicher. Verwandeln wir diese Knochen in etwas, mit dem die Menschen interagieren möchten. Heute arbeiten wir an der Mobile-First-Methode.

.stories

Für unseren <div class="stories">-Container benötigen wir einen horizontalen Scroll-Container. Dies erreichen wir durch:

  • Den Container als Raster verwenden
  • Festlegen, dass jedes untergeordnete Element den Zeilen-Track füllt
  • Die Breite der einzelnen untergeordneten Elemente an die Breite des Darstellungsbereichs eines Mobilgeräts anpassen

Im Raster werden weiterhin neue 100vw-breite Spalten rechts neben der vorherigen eingefügt, bis alle HTML-Elemente im Markup platziert sind.

Chrome und die Entwicklertools werden in einem Raster geöffnet, das das Layout in voller Breite zeigt
Chrome-Entwicklertools, die einen Überlauf der Rasterspalten anzeigen und einen horizontalen Scroller ermöglichen

Fügen Sie am Ende von app/css/index.css den folgenden CSS-Code ein:

.stories {
  display: grid;
  grid: 1fr / auto-flow 100%;
  gap: 1ch;
}

Da nun Inhalte über den Darstellungsbereich hinausreichen, ist es an der Zeit, diesem Container mitzuteilen, wie er damit umgehen soll. Fügen Sie Ihrem .stories-Regelsatz die markierten Codezeilen hinzu:

.stories {
  display: grid;
  grid: 1fr / auto-flow 100%;
  gap: 1ch;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior: contain;
  touch-action: pan-x;
}

Wir möchten horizontales Scrollen, also setzen wir overflow-x auf auto. Wenn der Nutzer scrollt, soll sich die Komponente vorsichtig auf der nächsten Story befinden. Deshalb verwenden wir scroll-snap-type: x mandatory. Weitere Informationen zu diesem CSS finden Sie in den Abschnitten CSS Scroll Snap Points und Overscroll-Verhalten in meinem Blogpost.

Es werden sowohl der übergeordnete Container als auch die untergeordneten Container benötigt, um dem Scroll-Andocken zuzustimmen. Gehen wir also jetzt darum. Fügen Sie am Ende von app/css/index.css den folgenden Code hinzu:

.user {
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

Deine Anwendung funktioniert noch nicht. Im Video unten siehst du, was passiert, wenn scroll-snap-type aktiviert und deaktiviert ist. Wenn diese Option aktiviert ist, springt jedes horizontale Scrollen zur nächsten Story. Wenn diese Option deaktiviert ist, verwendet der Browser das standardmäßige Scrollverhalten.

Dadurch können Sie durch Ihre Freunde scrollen, aber es gibt immer noch ein Problem mit den Geschichten, die es zu lösen gilt.

.user

Lassen Sie uns im Abschnitt .user ein Layout erstellen, durch das diese untergeordneten Story-Elemente an der richtigen Stelle angeordnet werden. Wir verwenden einen praktischen Stapeltrick, um dieses Problem zu lösen. Wir erstellen im Wesentlichen ein 1 × 1-Raster, bei dem die Zeile und die Spalte denselben Alias für das Raster wie [story] haben und jedes Story-Rasterelement versucht, diesen Platz zu beanspruchen, was zu einem Stapel führt.

Fügen Sie Ihrem Regelsatz .user den markierten Code hinzu:

.user {
  scroll-snap-align: start;
  scroll-snap-stop: always;
  display: grid;
  grid: [story] 1fr / [story] 1fr;
}

Fügen Sie am Ende von app/css/index.css den folgenden Regelsatz hinzu:

.story {
  grid-area: story;
}

Ohne absolute Positionierung, Gleitkommazahlen oder andere Layoutanweisungen, die ein Element aus dem Ablauf entfernen, sind wir immer noch im Fluss. Außerdem ist es so gut wie kein Code, seht euch das mal an! Das wird im Video und im Blogpost genauer erklärt.

.story

Jetzt müssen wir nur noch das Story-Element gestalten.

Wie bereits erwähnt, ist das Attribut style für jedes <article>-Element Teil einer Technik zum Laden von Platzhaltern:

<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>

Wir verwenden die Eigenschaft background-image, mit der wir mehr als ein Hintergrundbild angeben können. Wir können sie so anordnen, dass das Bild oben angezeigt wird, sobald der Ladevorgang abgeschlossen ist. Dazu fügen wir die Bild-URL in eine benutzerdefinierte Eigenschaft (--bg) ein und verwenden sie in unserem CSS, um den Ladeplatzhalter zu überlagern.

Zuerst aktualisieren wir den Regelsatz .story, um einen Farbverlauf durch ein Hintergrundbild zu ersetzen, sobald der Ladevorgang abgeschlossen ist. Fügen Sie Ihrem Regelsatz .story den markierten Code hinzu:

.story {
  grid-area: story;

  background-size: cover;
  background-image:
    var(--bg),
    linear-gradient(to top, lch(98 0 0), lch(90 0 0));
}

Wenn Sie background-size auf cover festlegen, bleibt im Darstellungsbereich kein Leerraum, da unser Bild ihn füllt. Wenn Sie zwei Hintergrundbilder definieren, können wir einen praktischen CSS-Webtrick namens loading tombstone ausführen:

  • Hintergrundbild 1 (var(--bg)) ist die URL, die wir inline im HTML-Code übergeben haben.
  • Hintergrundbild 2 (linear-gradient(to top, lch(98 0 0), lch(90 0 0)) ist ein Farbverlauf, der angezeigt wird, während die URL geladen wird)

CSS ersetzt den Farbverlauf automatisch durch das Bild, sobald das Bild heruntergeladen wurde.

Als Nächstes fügen wir CSS hinzu, um Verhaltensweisen zu beseitigen, sodass der Browser schneller arbeiten kann. Fügen Sie Ihrem Regelsatz .story den markierten Code hinzu:

.story {
  grid-area: story;

  background-size: cover;
  background-image:
    var(--bg),
    linear-gradient(to top, lch(98 0 0), lch(90 0 0));

  user-select: none;
  touch-action: manipulation;
}
  • user-select: none verhindert, dass Nutzer versehentlich Text auswählen
  • touch-action: manipulation weist den Browser an, diese Interaktionen als Touch-Ereignisse zu behandeln, sodass der Browser nicht entscheiden kann, ob du auf eine URL klickst oder nicht

Zuletzt fügen wir CSS-Code hinzu, um den Übergang zwischen den Stories zu animieren. Fügen Sie den markierten Code in Ihren .story-Regelsatz ein:

.story {
  grid-area: story;

  background-size: cover;
  background-image:
    var(--bg),
    linear-gradient(to top, lch(98 0 0), lch(90 0 0));

  user-select: none;
  touch-action: manipulation;

  transition: opacity .3s cubic-bezier(0.4, 0.0, 1, 1);

  &.seen {
    opacity: 0;
    pointer-events: none;
  }
}

Die Klasse .seen wird einer Story hinzugefügt, die einen Exit benötigt. Ich habe die benutzerdefinierte Easing-Funktion (cubic-bezier(0.4, 0.0, 1,1)) aus dem Easing-Leitfaden von Material Design abgerufen (scrollen Sie zum Abschnitt Beschleunigtes Easing).

Wenn Sie genau hinsehen, ist Ihnen wahrscheinlich die Deklaration pointer-events: none aufgefallen und Sie kratzen gerade am Kopf. Ich würde sagen, das ist bisher der einzige Nachteil der Lösung. Wir benötigen sie, da oben ein .seen.story-Element platziert wird und Tippaktionen erhält, obwohl es unsichtbar ist. Wenn wir pointer-events auf none setzen, wandeln wir die Glass Story in ein Fenster um und stehlen keine weiteren Nutzerinteraktionen. Hier in unserem CSS-Code ist das kein allzu schwächerer Kompromiss und auch keine allzu schwer zu verwalten. Wir jonglieren nicht z-index. Ich fühle mich dabei immer noch gut.

JavaScript

Die Interaktionen der Komponente „Stories“ sind für den Nutzer ganz einfach: Durch Tippen auf die rechte Seite gelangst du zur nächsten. Einfache Dinge sind für die Nutzenden in der Regel harte Arbeit für Entwickelnde. Wir kümmern uns jedoch um viele Schritte.

Einrichtung

Zunächst berechnen und speichern wir so viele Informationen wie möglich. Fügen Sie den folgenden Code zu app/js/index.js hinzu:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

Die erste JavaScript-Zeile erfasst und speichert einen Verweis auf den Stamm des primären HTML-Elements. Die nächste Zeile berechnet, wo sich die Mitte des Elements befindet, sodass wir entscheiden können, ob ein Tippen vorwärts oder rückwärts erfolgen soll.

Status

Als Nächstes erstellen wir ein kleines Objekt mit einem für unsere Logik relevanten Zustand. In diesem Fall interessiert uns nur die aktuelle Meldung. In unserem HTML-Markup können wir darauf zugreifen, indem wir den ersten Freund und seinen aktuellsten Erfahrungsbericht greifen. Fügen Sie den markierten Code in die Datei app/js/index.js ein:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

const state = {
  current_story: stories.firstElementChild.lastElementChild
}

Listener

Wir haben jetzt genug Logik, um auf Nutzerereignisse zu warten und sie anzuweisen.

Maus

Sehen wir uns zuerst das Ereignis 'click' im Container „Stories“ an. Fügen Sie den markierten Code in app/js/index.js ein:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

const state = {
  current_story: stories.firstElementChild.lastElementChild
}

stories.addEventListener('click', e => {
  if (e.target.nodeName !== 'ARTICLE')
    return

  navigateStories(
    e.clientX > median
      ? 'next'
      : 'prev')
})

Wenn ein Klick erfolgt und nicht auf ein <article>-Element erfolgt, wird der Vorgang abgebrochen und nichts unternommen. Wenn es sich um einen Artikel handelt, erfassen wir die horizontale Position der Maus oder des Fingers mit clientX. navigateStories wurde noch nicht implementiert, aber das Argument gibt an, in welche Richtung wir gehen müssen. Wenn diese Nutzerposition über dem Medianwert liegt, wissen wir, dass wir zu next gehen müssen, andernfalls zu prev (vorherige).

Tastatur

Hören wir nun auf Tastatureingaben. Durch Drücken des Abwärtspfeils wechseln Sie zu next. Mit dem Aufwärtspfeil gehen wir zu prev.

Fügen Sie den markierten Code in app/js/index.js ein:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

const state = {
  current_story: stories.firstElementChild.lastElementChild
}

stories.addEventListener('click', e => {
  if (e.target.nodeName !== 'ARTICLE')
    return

  navigateStories(
    e.clientX > median
      ? 'next'
      : 'prev')
})

document.addEventListener('keydown', ({key}) => {
  if (key !== 'ArrowDown' || key !== 'ArrowUp')
    navigateStories(
      key === 'ArrowDown'
        ? 'next'
        : 'prev')
})

Navigation für Stories

Es wird Zeit, sich mit der einzigartigen Geschäftslogik von Storys und der UX zu befassen, für die sie berühmt geworden sind. Das sieht knifflig und knifflig aus, aber ich denke, es ist ziemlich leicht zu lesen, wenn Sie es Zeile für Zeile lesen.

Zu Beginn stecken wir einige Auswahlelemente, die uns bei der Entscheidung helfen, ob wir zu einem Freund scrollen oder eine Geschichte ein-/ausblenden sollen. Da wir im HTML-Code arbeiten, fragen wir ihn nach Freunden (Nutzern) oder Stories (Geschichte) ab.

Diese Variablen helfen uns bei der Beantwortung von Fragen wie: „Bedeutet „Weiter“, dass zu einer anderen Geschichte desselben Freundes oder eines anderen Freundes gewechselt wird?“ Dabei habe ich unsere Baumstruktur verwendet, um Eltern und ihre Kinder zu erreichen.

Fügen Sie am Ende von app/js/index.js den folgenden Code hinzu:

const navigateStories = direction => {
  const story = state.current_story
  const lastItemInUserStory = story.parentNode.firstElementChild
  const firstItemInUserStory = story.parentNode.lastElementChild
  const hasNextUserStory = story.parentElement.nextElementSibling
  const hasPrevUserStory = story.parentElement.previousElementSibling
}

Hier ist unser Ziel der Geschäftslogik so nah wie möglich an natürlicher Sprache:

  • Legen Sie fest, wie mit dem Tippen verfahren werden soll.
    • Wenn eine nächste/vorherige Meldung vorhanden ist: diese Geschichte zeigen
    • Wenn es die letzte/erste Geschichte des Freundes ist: Zeige einen neuen Freund
    • Wenn es keine Story in diese Richtung gibt: nichts tun
  • Neue aktuelle Story in state speichern

Fügen Sie der Funktion navigateStories den markierten Code hinzu:

const navigateStories = direction => {
  const story = state.current_story
  const lastItemInUserStory = story.parentNode.firstElementChild
  const firstItemInUserStory = story.parentNode.lastElementChild
  const hasNextUserStory = story.parentElement.nextElementSibling
  const hasPrevUserStory = story.parentElement.previousElementSibling

  if (direction === 'next') {
    if (lastItemInUserStory === story && !hasNextUserStory)
      return
    else if (lastItemInUserStory === story && hasNextUserStory) {
      state.current_story = story.parentElement.nextElementSibling.lastElementChild
      story.parentElement.nextElementSibling.scrollIntoView({
        behavior: 'smooth'
      })
    }
    else {
      story.classList.add('seen')
      state.current_story = story.previousElementSibling
    }
  }
  else if(direction === 'prev') {
    if (firstItemInUserStory === story && !hasPrevUserStory)
      return
    else if (firstItemInUserStory === story && hasPrevUserStory) {
      state.current_story = story.parentElement.previousElementSibling.firstElementChild
      story.parentElement.previousElementSibling.scrollIntoView({
        behavior: 'smooth'
      })
    }
    else {
      story.nextElementSibling.classList.remove('seen')
      state.current_story = story.nextElementSibling
    }
  }
}

Ausprobieren

  • Um die Website als Vorschau anzusehen, wählen Sie App ansehen und dann Vollbild Vollbild aus.

Fazit

Ich bin nun zum Abschluss für die Anforderungen, die ich an die Komponente hatte. Bauen Sie sie einfach auf, nutzen Sie Daten und machen Sie sie im Allgemeinen zu Ihrem Unternehmen!