Создание компонента вкладок

Общий обзор подхода к созданию компонента вкладок, аналогичного используемым в приложениях для iOS и Android.

В этой статье я хочу поделиться своими мыслями о создании компонента вкладок для веб-сайтов, который будет адаптивным и совместимым с различными устройствами и браузерами. Посмотрите демопример.

Демопример

Если вы предпочитаете видео, вот версия этой статьи на YouTube:

Обзор

Вкладки — стандартный компонент систем дизайна, но они могут быть разных видов и форм. Первыми были вкладки для ПК, построенные на элементе <frame>, теперь же у нас есть красивые мобильные компоненты с анимацией на основе «физических» свойств. Но задача у них всех одна: сэкономить место.

Сегодня основной элемент взаимодействия с вкладками — область навигации с кнопками, которые переключают видимость контента в отображаемом фрейме. Различные области контента занимают одно и то же пространство, но отображаются условно в зависимости от кнопки, выбранной в элементе навигации.

Коллаж довольно хаотичный из-за огромного разнообразия стилей, которые применяются в веб-дизайне к концепции этого компонента
Коллаж, представляющий различные стили компонента вкладок в веб-дизайне за последние 10 лет

Реализация на веб-платформе

В целом, сделать такой компонент оказалось довольно просто — благодаря нескольким важным функциям веб-платформы:

  • scroll-snap-points — взаимодействие жестами и с помощью клавиатуры, а также правильные позиции остановки прокрутки;
  • ссылки на контент посредством URL-хешей — поддержка встроенной прокрутки в браузере и передачи ссылок;
  • поддержка программ чтения с экрана: разметка элементами <a> и id="#hash";
  • prefers-reduced-motion — плавные переходы и мгновенная прокрутка внутри страницы;
  • предложенная функция @scroll-timeline для динамического подчеркивания и изменения цвета выбранной вкладки.

HTML-код

Принцип работы здесь такой: нажимаем ссылку, в которой URL-адрес представляет состояние вложенной страницы, а затем обновляем область контента, пока браузер прокручивает до соответствующего элемента.

У нас используются элементы структурного контента: ссылки и :target. Нам нужен список ссылок (для чего отлично подходит <nav>) и список элементов <article> (здесь подходит <section>). Каждому хешу ссылки будет соответствовать section, поэтому браузер сможет делать прокрутку по ссылке.

При нажатии кнопки с ссылкой меняется контент в фокусе

Например, в Chrome 89 при нажатии на ссылку фокус автоматически переключается на article-элемент :target — и не нужно ничего писать на JS. Пользователь может прокрутить содержимое article обычным образом с помощью имеющегося устройства ввода. Это дополнительный контент, как указано в разметке.

Например, в Chrome 89 при нажатии на ссылку фокус автоматически переключается на article-элемент :target — и не нужно ничего писать на JS. Пользователь может прокрутить содержимое article обычным образом с помощью имеющегося устройства ввода. Это дополнительный контент, как указано в разметке.

<snap-tabs>
  <header>
    <nav>
      <a></a>
      <a></a>
      <a></a>
      <a></a>
    </nav>
  </header>
  <section>
    <article></article>
    <article></article>
    <article></article>
    <article></article>
  </section>
</snap-tabs>

Установить связь между элементами <a> и <article> можно с помощью свойств hrefи id следующим образом:

<snap-tabs>
  <header>
    <nav>
      <a href="#responsive"></a>
      <a href="#accessible"></a>
      <a href="#overscroll"></a>
      <a href="#more"></a>
    </nav>
  </header>
  <section>
    <article id="responsive"></article>
    <article id="accessible"></article>
    <article id="overscroll"></article>
    <article id="more"></article>
  </section>
</snap-tabs>

Затем я заполнил каждый article различным количеством «рыбы», а ссылки — заголовками различной длины с изображениями для заголовков. Контент у нас есть — можно приступать к работе над макетом.

Макеты с прокруткой

В этом компоненте есть области прокрутки трех типов:

  • Блок навигации (розовый цвет) использует горизонтальную прокрутку.
  • Область контента (синий цвет) также использует горизонтальную прокрутку.
  • Элементы article (зеленый цвет) используют вертикальную прокрутку.
Три цветных прямоугольника со стрелками соответствующего цвета, которые указывают область и направление прокрутки

При прокрутке используются элементы двух типов:

  1. Окно.
    Прямоугольник с заданными размерами и стилем свойства overflow.
  2. Безразмерная поверхность.
    В этом макете это списочные контейнеры: ссылки nav, элементы article в разделах section и содержимое article.

Макет для <snap-tabs>

В качестве макета верхнего уровня я выбрал flex (адаптируемый блок) с направлением column — чтобы заголовок и section располагались вертикально. Это первое окно прокрутки; оно скрывает всё с помощью overflow: hidden. Заголовок и и section будут использовать прокрутку за границы в виде отдельных зон.

HTML
<snap-tabs>
  <header></header>
  <section></section>
</snap-tabs>
CSS
snap-tabs {
  display: flex;
  flex-direction: column;

  /* устанавливаем первичный контейнер */
  overflow: hidden;
  position: relative;

  & > section {
    /* указываем использовать всё место */
    block-size: 100%;
  }

  & > header {
    /* защита от случая, когда 
требует 100 % */ flex-shrink: 0; /* учет особенностей различных браузеров */ min-block-size: fit-content; } }

Возвращаясь к разноцветной схеме с тремя областями прокрутки:

  • Элемент <header> теперь готов стать розовым контейнером прокрутки.
  • Элемент <section> готов стать синим контейнером прокрутки.

Фреймы, которые я выделил ниже с помощью VisBug, помогают увидеть окна, созданные контейнерами прокрутки.

Элементы «header» и «section» помечены ярко-розовыми ярлыками и выделены рамкой, ограничивающей место, которое они занимают в компоненте

Макет для <header>

Следующий макет почти такой же: я создаю вертикальную упорядоченную структуру с помощью flex.

HTML
<snap-tabs>
  <header>
    <nav></nav>
    <span class="snap-indicator"></span>
  </header>
  <section></section>
</snap-tabs>
CSS
header {
  display: flex;
  flex-direction: column;
}

Элемент .snap-indicator должен перемещаться горизонтально вместе с группой ссылок, и такой макет для header позволяет этого добиться. Ни одного элемента с абсолютным размещением!

Элементы «nav» и «span.indicator» помечены ярко-розовыми ярлыками и выделены рамкой, ограничивающей место, которое они занимают в компоненте

Далее — стили прокрутки. Оказывается, один стиль можно использовать в двух областях горизонтальной прокрутки (header и section), поэтому я сделал вспомогательный класс: .scroll-snap-x.

.scroll-snap-x {
  /* браузер решает, можно ли прокручивать и отображать полосы по X, Y скрыто */
  overflow: auto hidden;
  /* не даем создать цепочку прокрутки по X */
  overscroll-behavior-x: contain;
  /* прокрутка должна привязываться к дочернему элементу по X */
  scroll-snap-type: x mandatory;

  @media (hover: none) {
    scrollbar-width: none;

    &::-webkit-scrollbar {
      width: 0;
      height: 0;
    }
  }
}

В каждом случае нужен overflow по оси x, contain для захвата выхода за границы прокрутки, скрытые полосы прокрутки для сенсорных устройств и, наконец, scroll-snap для фиксации областей показа контента. Удобный порядок вкладок при использовании клавиатуры позволяет переключать фокус естественный образом. У контейнеров scroll-snap красивый «карусельный» стиль взаимодействия при использовании с клавиатуры.

Макет для <nav> в заголовке

Ссылки nav должны располагаться строкой, без разрывов строк, с центрированием по вертикали, причем каждый элемент ссылки должен привязываться к контейнеру scroll-snap. CSS 2021 отлично с этим справляется!

HTML
<nav>
  <a></a>
  <a></a>
  <a></a>
  <a></a>
</nav>
CSS
nav {
  display: flex;

  & a {
    scroll-snap-align: start;

    display: inline-flex;
    align-items: center;
    white-space: nowrap;
  }
}

Стили и размеры ссылок задаются автоматически, поэтому в макете nav нужно указать только направление и структуру наполнения — flow. Благодаря различной ширине элементов nav за переходом между вкладками интересно наблюдать: ширина индикатора подстраивается к новой цели. Отображение полосы прокрутки браузером будет зависеть от количества элементов.

Элементы «nav» помечены ярко-розовыми ярлыками, выделены рамкой, указывающей место, которое они занимают в компоненте, и снабжены стрелкой для направления развертывания

Макет для <section>

Этот раздел представляет собой элемент flex и должен быть основным потребителем места. Ему также необходимо создать столбцы для размещения статей. И CSS 2021 снова отлично справляется с задачей! С помощью block-size: 100% элемент растягивается на весь родительский объект, а затем для собственного макета создает несколько столбцов с шириной, равной 100% родительского объекта. Здесь проценты использовать удобно, поскольку для «родителя» мы указали строгие ограничения.

HTML
<section>
  <article></article>
  <article></article>
  <article></article>
  <article></article>
</section>
CSS
section {
  block-size: 100%;

  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
}

Это можно перевести как «расширять по вертикали, насколько это возможно» (вспомните заголовок, для которого мы установили на flex-shrink: 0: это защита от такого принудительного расширения). Так мы устанавливаем высоту строки для столбцов полной высоты. При этом стиль auto-flow указывает сетке всегда располагать дочерние элементы в горизонтальную линию, без переноса (как нам и нужно), что позволяет заполнить родительское окно с выходом за его границы.

Элементы «article» помечены ярко-розовыми ярлыками, выделены рамкой, указывающей место, которое они занимают в компоненте, и снабжены стрелкой для направления развертывания

Иногда мне бывает трудно понять, что к чему! Этот элемент section вписан в прямоугольник, но при этом также создает набор прямоугольников. Надеюсь, рисунки и текст помогут вам разобраться.

Макет для <article>

Пользователю необходимо дать возможность прокручивать содержимое article, причем полосы прокрутки должны появляются только при переполнении. Эти элементы article находятся в интересном состоянии: они одновременно являются и родительскими, и дочерними элементами прокрутки. Браузер сам обрабатывает разнообразные взаимодействия с сенсорным экраном, мышью и клавиатурой, так что нам об этом беспокоиться не нужно.

HTML
<article>
  <h2></h2>
  <p></p>
  <p></p>
  <h2></h2>
  <p></p>
  <p></p>
  ...
</article>
CSS
article {
  scroll-snap-align: start;

  overflow-y: auto;
  overscroll-behavior-y: contain;
}

Я решил, что article будут привязаны к их родительскому компоненту прокрутки. Мне нравится, что элементы link навигации и элементы article привязываются к началу соответствующих контейнеров прокрутки: создается ощущение гармоничных взаимоотношений.

Элемент «article» и его дочерние элементы помечены ярко-розовыми ярлыками, выделены рамкой, указывающей место, которое они занимают в компоненте, и снабжены стрелкой для направления развертывания

Элемент article является дочерней сеткой, причем ее размер предопределен как область просмотра, в которой нам нужна прокрутка. Это означает, что стили высота и ширины здесь не нужны — достаточно определить переполнение. Для overflow-y я задаю auto, а затем захватываю взаимодействие прокрутки с помощью удобного свойства overscroll-behavior.

Резюме по трем областям прокрутки

Ниже в настройках системы у меня выбрано «всегда показывать полосы прокрутки». Мне кажется важным сделать так, что макет работал, когда этот параметр включен: это позволяет проверить макет и оркестровку прокрутки.

Включен показ трех полос прокрутки. Они занимают место в макете, но наш компонент по-прежнему выглядит отлично

Я думаю, что наличие контейнера для полос прокрутки в этом компоненте помогает четко показать, где находятся области прокрутки, в каком направлении они работают и как взаимодействуют друг с другом. Каждый из этих фреймов окна прокрутки также является родительским элементом flex или grid по отношению к макету.

DevTools помогают визуализировать структуру и поведение макета:

Области прокрутки с обозначением инструментов `grid` и `flex`, указывающим место, которое они занимают в компоненте, и направление развертывания
Chromium DevTools: макет для элемента `nav` адаптируемого блока, содержащий элементы со ссылками, макет `section` в виде сетки с элементами `article`, а также элементы `article` с абзацами и элементами заголовков.

Макеты прокрутки готовы: с привязкой, ссылками на контент и возможностью использования с клавиатуры. Прочная основа для улучшения удобства использования, стиля и отличная возможность насладиться результатом.

О функциях

Дочерние элементы с привязкой к прокрутке сохраняют зафиксированное положение при изменении размера. Это означает, что коду JavaScript не нужно ничего отображать при повороте устройства или изменении размера браузера. Перейдите в Режим устройства (Device Mode) в Chromium DevTools и выберите любой режим, кроме отзывчивого (Responsive), а затем измените размер фрейма устройства. Элемент остается в поле зрения и фиксируется вместе с содержимым. Эта функция работает так с того момента, как Chromium обновил реализацию в соответствии со спецификацией. Можете почитать запись в блоге об этом.

Анимация

Цель анимации здесь — четко связать работу ссылок с откликом интерфейса. Так мы поможем пользователю с удобством (надеюсь) просмотреть весь контент. Я буду добавлять анимацию движения с определенной целью и и условиями. Надо помнить, что пользователи могут задавать предпочтения по движению в операционной системе, и я с удовольствием учитываю их в проектируемых мной интерфейсах.

Я свяжу подчеркивание вкладки с положением прокрутки элемента article. Привязка обеспечивает не только красивое выравнивание, но и соотнесение с началом и окончанием анимации. Это позволяет элементу <nav>, который действует как мини-карта, не терять связь с контентом. Предпочтения пользователя по движению будем проверять и с помощью CSS, и с помощью JS. В некоторых местах придется проявить особую внимательность!

Поведение прокрутки

Мы можем улучшить поведение :target и element.scrollIntoView(). По умолчанию переход мгновенный: браузер просто устанавливает положение прокрутки. Допустим, мы хотим сделать переход в положение прокрутки не мгновенным.

@media (prefers-reduced-motion: no-preference) {
  .scroll-snap-x {
    scroll-behavior: smooth;
  }
}

Поскольку у нас здесь появляется движение — причем такое, которое пользователь не контролирует (прокрутка), мы применяем этот стиль, только если у пользователя в операционной системе не заданы предпочтения по ограничению отображаемого движения. То есть, мы анимируем прокрутку только для тех пользователей, которые не против этого.

Индикатор вкладок

Цель этой анимации — связать индикатор с состоянием контента. Для пользователей, предпочитающих ограничение движения, я решил делать переход цвета в стилях border-bottom, а для остальных — связанный с прокруткой сдвиг и анимацию смены цвета.

Переключая это предпочтение в Chromium DevTools, я демонстрирую оба стиля переходов. Работать над ними было очень интересно!

@media (prefers-reduced-motion: reduce) {
  snap-tabs > header a {
    border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
    transition: color .7s ease, border-color .5s ease;

    &:is(:target,:active,[active]) {
      color: var(--text-active-color);
      border-block-end-color: hsl(var(--accent));
    }
  }

  snap-tabs .snap-indicator {
    visibility: hidden;
  }
}

Если пользователь хочет видеть меньше движения, я скрываю .snap-indicator, поскольку этот элемент становится не нужен. Вместо него я использую стили border-block-end и transition. Также обратите внимание на взаимодействие вкладок: активный элемент nav выделяется не только подчеркиванием, но и более тёмным цветом текста. У активного элемента более высокий цветовой контраст текста и яркий акцент подчеркивания.

Всего пара строк CSS-кода — и мы позаботились о каждом (в том смысле, что учитываем предпочтения пользователей по движению на странице). Мне нравится.

@scroll-timeline

Выше я показал, как работаю со стилями плавного перехода, когда нужно ограничить движение. В этом разделе я покажу, как связать индикатор и область прокрутки. Далее будет несколько интересных экспериментов — надеюсь, вы сгораете от нетерпения.

const { matches:motionOK } = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
);

Сначала я с помощью JavaScript проверяю предпочтения пользователя по движению. Если результат — false (пользователь хочет видеть меньше движения), тогда мы не будем запускать эффекты привязки прокрутки.

if (motionOK) {
  // код анимации с использованием движения
}

На момент написания статьи поддержки @scroll-timeline в браузерах нет. Функция пребывает в виде черновой спецификации — есть только экспериментальные реализации. Однако для нее есть полифил, который я и применяю в этой демонстрации.

ScrollTimeline

И CSS, и JavaScript позволяют делать ScrollTimeline для прокрутки, однако я выбрал JavaScript — чтобы использовать в анимации актуальные размеры элементов.

const sectionScrollTimeline = new ScrollTimeline({
  scrollSource: tabsection,  // snap-tabs > section
  orientation: 'inline',     // прокрутка в направлении потока букв
  fill: 'both',              // двунаправленное связывание
});

Я хочу, чтобы один элемент следовал за положением прокрутки другого. Создав ScrollTimeline, я определяю ведущий элемент для связки с прокруткой — scrollSource. Обычно анимация в веб-дизайне запускается в соответствии с глобальным тиком интервала времени, но с помощью sectionScrollTimeline это можно изменить.

tabindicator.animate({
    transform: ...,
    width: ...,
  }, {
    duration: 1000,
    fill: 'both',
    timeline: sectionScrollTimeline,
  }
);

Прежде чем я перейду к ключевым кадрам анимации, думаю, важно упомянуть, что ведомый элемент прокрутки — tabindicator — будет анимироваться по специальной временной шкале — прокрутке нашего section. Здесь мы заканчиваем привязку, но у нас отсутствует последний ингредиент — точки с отслеживанием состояния, между которыми происходит анимация. Их еще называют ключевыми кадрами.

Динамические ключевые кадры

Есть, конечно, очень мощный чисто декларативный способ делать анимацию в CSS с помощью @scroll-timeline, но нужная мне анимация была слишком динамичной. CSS не позволяет делать переход между шириной со значением auto и динамически создавать ключевые кадры в зависимости от длины дочерних элементов.

Однако получить эту информацию можно с помощью JavaScript. Поэтому мы будем проходить по дочерним элементам сами и захватывать вычисленные значения во время выполнения кода:

tabindicator.animate({
    transform: [...tabnavitems].map(({offsetLeft}) =>
      `translateX(${offsetLeft}px)`),
    width: [...tabnavitems].map(({offsetWidth}) =>
      `${offsetWidth}px`)
  }, {
    duration: 1000,
    fill: 'both',
    timeline: sectionScrollTimeline,
  }
);

Для каждого tabnavitem деструктурируем положение offsetLeft и возвращаем строку, которая использует его как значение translateX. Так мы получаем четыре ключевых кадра преобразования для анимации. То же делаем и с шириной: запрашиваем ее у каждого элемента и используем как значение ключевого кадра.

Вот пример вывода с моими шрифтами и настройками браузера:

Ключевые кадры translateX:

[...tabnavitems].map(({offsetLeft}) =>
  `translateX(${offsetLeft}px)`)

// возвращает четыре элемента массива, представляющие собой четыре состояния ключевых кадров
// ["translateX(0px)", "translateX(121px)", "translateX(238px)", "translateX(464px)"]

Ключевые кадры ширины:

[...tabnavitems].map(({offsetWidth}) =>
  `${offsetWidth}px`)

// возвращает четыре элемента массива, представляющие собой четыре состояния ключевых кадров
// ["121px", "117px", "226px", "67px"]

Стратегия вкратце: индикатор вкладки будет анимироваться по четырем ключевым кадрам в зависимости от положения scroll-snap компонента прокрутки section. Точки привязки задают четкое разграничение между ключевыми кадрами и делают анимацию более синхронной по ощущениям.

Активная и неактивная вкладки с оверлеями VisBug, которые показывают оценку контрастности и соответствие требованиям для них

Пользователь управляет анимацией своими действиями и видит, как ширина и положение индикатора меняются от одного section к другому, в точности следуя за прокруткой.

Возможно, вы не заметили, но я очень горжусь тем, как получилось сделать переход цвета при выборе выделенного элемента навигации.

Невыбранный элемент светло-серого цвета становится еще менее выразительным в сравнении с более контрастным выделенным. Обычно меняют цвет текста — например, при наведении курсора и выделении, — а вот делать переход цвета при прокрутке синхронно с индикатором подчеркивания — это уже следующий уровень.

Как это делается:

tabnavitems.forEach(navitem => {
  navitem.animate({
      color: [...tabnavitems].map(item =>
        item === navitem
          ? `var(--text-active-color)`
          : `var(--text-color)`)
    }, {
      duration: 1000,
      fill: 'both',
      timeline: sectionScrollTimeline,
    }
  );
});

Для каждой ссылки nav на вкладке нужна новая анимация цвета по той же временно́й шкале, что и у индикатора подчеркивания. Я использую ту же временну́ю шкалу, что и раньше: ее задача — выдавать при прокрутке тик, поэтому мы можем использовать его в анимации любого нужного нам типа. Как и раньше, я создаю четыре ключевых кадра в цикле и получаю цвета.

[...tabnavitems].map(item =>
  item === navitem
    ? `var(--text-active-color)`
    : `var(--text-color)`)

// возвращает четыре элемента массива, представляющие собой четыре состояния ключевых кадров
// [
  "var(--text-active-color)",
  "var(--text-color)",
  "var(--text-color)",
  "var(--text-color)",
]

Ключевой кадр с цветом var(--text-active-color) выделяет ссылку. В остальных случаях это стандартный цвет текста. Вложенный цикл делает процедуру вполне понятной: внешний цикл — это каждый элемент навигации, а внутренний — это их ключевые кадры. Я проверяю, совпадает ли элемент внешнего цикла с элементом внутреннего цикла и таким образом узнаю, когда он выбран.

Писать этот код — одно удовольствие.

Как еще улучшить наш JavaScript

Напомню: то, что я здесь показываю, в основе своей работает и без JavaScript. Однако давайте посмотрим, что можно улучшить, используя JS.

Ссылки на контент

Ссылки на контент — это скорее мобильный термин, но, думаю, что их цели вполне соответствует случай, когда URL-адрес передается непосредственно в содержимое вкладки. Браузер будет переходить на странице к идентификатору, совпадающим с хешем URL. Я обнаружил, что этот обработчик onload действует на всех платформах.

window.onload = () => {
  if (location.hash) {
    tabsection.scrollLeft = document
      .querySelector(location.hash)
      .offsetLeft;
  }
}

Синхронизация с окончанием прокрутки

Пользователи не всегда будут нажимать на вкладки или использовать клавиатуру — иногда они просто будут использовать прокрутку. Когда компонент прокрутки в section останавливается, его положение должно совпадать с состоянием верхней панели навигации.

Так мы ожидаем окончания прокрутки:

tabsection.addEventListener('scroll', () => {
  clearTimeout(tabsection.scrollEndTimer);
  tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100);
});

Каждый раз при прокрутке элементов section мы сбрасываем время ожидания (если оно есть) и начинаем новый отсчет. Когда прокрутка элемента section останавливается, мы не сбрасываем время ожидания, а запускаем отсчет 100 мс с момента начала бездействия. По окончании отсчета вызываем функцию, которая определяет, где пользователь остановился.

const determineActiveTabSection = () => {
  const i = tabsection.scrollLeft / tabsection.clientWidth;
  const matchingNavItem = tabnavitems[i];

  matchingNavItem && setActiveTab(matchingNavItem);
};

Поскольку у нас прокрутка с привязкой, то, разделив текущее положение прокрутки на ширину области прокрутки, мы должны получить целое, а не десятичное число. Затем я пытаюсь взять navitem из кеша с помощью вычисленного здесь индекса, и, если подходящий находится, делаю его активным.

const setActiveTab = tabbtn => {
  tabnav
    .querySelector(':scope a[active]')
    .removeAttribute('active');

  tabbtn.setAttribute('active', '');
  tabbtn.scrollIntoView();
};

Сначала мы деактивируем вкладку, которая является активной сейчас, а затем даем полученному элементу nav атрибут активного состояния. Здесь стоит отметить вызов scrollIntoView(), который интересным образом взаимодействует с CSS.

.scroll-snap-x {
  overflow: auto hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;

  @media (prefers-reduced-motion: no-preference) {
    scroll-behavior: smooth;
  }
}

Во вспомогательном CSS scroll-snap мы вложили запрос медиа, который применяет прокрутку типа smooth, если пользователь разрешает элементы с движением. JavaScript может легко вызывать элементы прокрутки в представление, а CSS может декларативно управлять интерфейсом. Очаровательная парочка.

Заключение

Я рассказал свое видение решения этой задачи. А как ее решали бы вы? Получилась очень интересная архитектура компонентов! Кто же первый сделает версию с блек-джеком в своем любимом фреймворке? 🙂

Давайте разнообразим наши подходы и рассмотрим самые разные реализации для веб-сайтов. Создайте свою версию на Glitch, твитните мне, и я добавлю добавлю ее в раздел Ремиксы сообщества ниже.

Ремиксы сообщества