Component patterns
A collection of cross browser UI components for use in accelerating or inspiring your own design systems.
On this page
Welcome to our growing collection of component patterns. These can provide
inspiration, accessibility requirements, adaptive strategies for viewport and
user preferences, and more.
Breadcrumbs
<nav class="breadcrumbs" role="navigation">
<a href="./home/">
<span class="crumbicon">
<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
<use href="#icon-home" />
</svg>
</span>
<span class="home-label">Home</span>
</a>
<span class="crumb-separator" aria-hidden="true">»</span>
<span class="crumb">
<a aria-current="page">Page A</a>
<span class="crumbicon">
<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
<use href="#icon-dropdown-arrow" />
</svg>
<select class="disguised-select" title="Navigate to another page">
<option selected>Page A</option>
<option>Page B</option>
<option>Page C</option>
</select>
</span>
</span>
</nav>
<svg style="display: none;">
<symbol id="icon-home">
<title>A home icon</title>
<path
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</symbol>
<symbol id="icon-dropdown-arrow">
<title>A down arrow</title>
<path d="M19 9l-7 7-7-7" />
</symbol>
</svg>
const crumbs = document.querySelectorAll('.breadcrumbs select')
const allowedKeys = new Set(['Tab', 'Enter', ' '])
const preventedKeys = new Set(['ArrowUp', 'ArrowDown'])
// watch crumbs for *full* changes,
// ensures it's not a user exploring options via keyboard
crumbs.forEach(nav => {
let ignoreChange = false
nav.addEventListener('change', e => {
if (ignoreChange) return
const option = e.target
const choice = option.value
const crumb = option.closest('.crumb')
// flag crumb so adjacent siblings can be hidden
crumb.classList.add('tree-changed')
// update crumb text to reflect the user's choice
crumb.querySelector(':scope > a').textContent = choice
routePage(choice)
})
nav.addEventListener('keydown', ({ key }) => {
if (preventedKeys.has(key))
ignoreChange = true
else if (allowedKeys.has(key))
ignoreChange = false
})
})
const routePage = route => {
console.info('change path to: ', route)
// change entire URL (window.location)
// or
// use your favorite clientside framework's router
}
.breadcrumbs {
--nav-gap: 2ch;
display: flex;
align-items: center;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x proximity;
gap: var(--nav-gap);
padding: calc(var(--nav-gap) / 2);
scroll-padding-inline: calc(var(--nav-gap) / 2);
& > a:first-of-type:not(.crumb) {
display: inline-flex;
align-items: center;
gap: calc(var(--nav-gap) / 4);
@media (width <= 480px) { & > .home-label {
display: none;
}}
}
& a {
text-underline-offset: .25em;
outline-offset: 3px;
/* fix Safari inaccessible dark color scheme links */
/* https://bugs.webkit.org/show_bug.cgi?id=226893 */
@media (prefers-color-scheme: dark) {
@supports (-webkit-hyphens:none) { &[href] {
color: hsl(240 100% 81%);
}}
}
}
& > .crumb:last-of-type {
scroll-snap-align: end;
}
@supports (-webkit-hyphens:none) {
scroll-snap-type: none;
}
}
.crumb {
display: inline-flex;
align-items: center;
gap: calc(var(--nav-gap) / 4);
& > a {
white-space: nowrap;
&[aria-current="page"] {
font-weight: bold;
}
}
&.tree-changed ~ * {
display: none;
}
}
.crumb-separator {
color: ButtonText;
}
.disguised-select {
inline-size: 100%;
block-size: 100%;
opacity: .01;
font-size: min(100%, 16px);
}
.crumbicon {
--size: 3ch;
display: grid;
grid: [stack] var(--size) / [stack] var(--size);
place-items: center;
border-radius: 50%;
--icon-shadow-size: 0px;
box-shadow: inset 0 0 0 var(--icon-shadow-size) currentColor;
@media (--motionOK) { & {
transition: box-shadow .2s ease;
}}
@nest .crumb:is(:focus-within, :hover) > & {
--icon-shadow-size: 1px;
}
@nest .crumb > &:is(:focus-within, :hover) {
--icon-shadow-size: 2px;
& svg {
stroke-width: 2px;
}
}
& > * {
grid-area: stack;
}
& > svg {
max-block-size: 100%;
margin: calc(var(--nav-gap) / 4);
stroke: currentColor;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1px;
}
}
This pattern shows how to create a responsive and accessible breadcrumbs component for users to navigate your site.
Full article · Video on YouTube · Source on Github
Buttons
<section>
<h2>9 button types</h2>
<p>Unified modern style, visual differences reinforce purpose.</p>
</section>
<button>Default</button>
<input type="button" value="<input>"/>
<button>
<svg viewBox="0 0 24 24" stroke="currentColor" width="24" height="24" aria-hidden="true">
<path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
Icon
</button>
<button type="submit">Submit</button>
<button type="button">Type Button</button>
<button type="reset">Reset</button>
<button disabled>Disabled</button>
<button class="btn-custom">Custom</button>
<input type="file">
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"],
input[type="file"]
),
:where(input[type="file"])::file-selector-button {
--_accent-light: hsl(210 100% 40%);
--_accent-dark: hsl(210 50% 70%);
--_accent: var(--_accent-light);
--_text-light: hsl(210 10% 30%);
--_text-dark: hsl(210 5% 95%);
--_text: var(--_text-light);
--_bg-light: hsl(0 0% 100%);
--_bg-dark: hsl(210 9% 31%);
--_bg: var(--_bg-light);
--_input-well-light: hsl(210 16% 87%);
--_input-well-dark: hsl(204 10% 10%);
--_input-well: var(--_input-well-light);
--_padding-inline: 1.75ch;
--_padding-block: .75ch;
--_border-radius: .5ch;
--_border-light: hsl(210 14% 89%);
--_border-dark: var(--_bg-dark);
--_border: var(--_border-light);
--_highlight-size: 0;
--_highlight-light: hsl(210 10% 71% / 25%);
--_highlight-dark: hsl(210 10% 5% / 25%);
--_highlight: var(--_highlight-light);
--_ink-shadow-light: 0 1px 0 var(--_border-light);
--_ink-shadow-dark: 0 1px 0 hsl(210 11% 15%);
--_ink-shadow: var(--_ink-shadow-light);
--_icon-size: 2ch;
--_icon-color: var(--_accent);
--_shadow-color-light: 220 3% 15%;
--_shadow-color-dark: 220 40% 2%;
--_shadow-color: var(--_shadow-color-light);
--_shadow-strength-light: 1%;
--_shadow-strength-dark: 25%;
--_shadow-strength: var(--_shadow-strength-light);
--_shadow-1: 0 1px 2px -1px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 9%));
--_shadow-2: 0 3px 5px -2px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 3%)),0 7px 14px -5px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 5%));
--_shadow-depth-light: 0 1px var(--_border-light);
--_shadow-depth-dark: 0 1px var(--_bg-dark);
--_shadow-depth: var(--_shadow-depth-light);
--_transition-motion-reduce: ;
--_transition-motion-ok:
box-shadow 145ms ease,
outline-offset 145ms ease
;
--_transition: var(--_transition-motion-reduce);
font: inherit;
letter-spacing: inherit;
line-height: 1.5;
border-radius: var(--_border-radius);
}
@media (prefers-color-scheme: dark) {
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"],
input[type="file"]
),
:where(input[type="file"])::file-selector-button {
--_bg: var(--_bg-dark);
--_text: var(--_text-dark);
--_border: var(--_border-dark);
--_accent: var(--_accent-dark);
--_highlight: var(--_highlight-dark);
--_input-well: var(--_input-well-dark);
--_ink-shadow: var(--_ink-shadow-dark);
--_shadow-depth: var(--_shadow-depth-dark);
--_shadow-color: var(--_shadow-color-dark);
--_shadow-strength: var(--_shadow-strength-dark);
}
}
@media (prefers-reduced-motion: no-preference) {
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"],
input[type="file"]
),
:where(input[type="file"])::file-selector-button {
--_transition: var(--_transition-motion-ok);
}
}
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"]
),
:where(input[type="file"])::file-selector-button {
cursor: pointer;
touch-action: manipulation;
font-size: var(--_size, 1rem);
background: var(--_bg);
color: var(--_text);
border: 2px solid var(--_border);
box-shadow:
var(--_shadow-2),
var(--_shadow-depth),
0 0 0 var(--_highlight-size) var(--_highlight)
;
text-shadow: var(--_ink-shadow);
transition: var(--_transition);
display: inline-flex;
justify-content: center;
align-items: center;
text-align: center;
gap: 1ch;
font-weight: 700;
padding-block: var(--_padding-block);
padding-inline: var(--_padding-inline);
user-select: none;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
}
/* icons */
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"]
) > :where(svg, [data-icon]) {
block-size: var(--_icon-size);
inline-size: var(--_icon-size);
stroke: var(--_icon-color);
filter: drop-shadow(var(--_ink-shadow));
flex-shrink: 0;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
/* focus */
:where(button, input):where(:not(:active)):focus-visible {
outline-offset: 5px;
}
/* pressing */
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"]
):where(:not(:active):hover) {
--_highlight-size: .5rem;
}
/* disabled */
:where(
button,
input[type="button"],
input[type="submit"],
input[type="reset"]
)[disabled] {
--_bg: none;
--_text-light: hsl(210 7% 40%);
--_text-dark: hsl(210 11% 71%);
cursor: not-allowed;
box-shadow: var(--_shadow-1);
}
/* adaptive indigo text */
:where(
[type="submit"],
form button:not([type],[disabled])
) {
--_text: var(--_accent);
}
/* red reset */
:where([type="reset"]) {
--_border-light: hsl(0 100% 83%);
--_highlight-light: hsl(0 100% 89% / 20%);
--_text-light: hsl(0 80% 50%);
--_text-dark: hsl(0 100% 89%);
}
:where([type="reset"]):focus-visible {
outline-color: currentColor;
}
/* file input */
:where(input[type="file"]) {
inline-size: 100%;
max-inline-size: max-content;
background-color: var(--_input-well);
}
:where(input[type="button"]),
:where(input[type="file"])::file-selector-button {
appearance: none;
}
:where(input[type="file"])::file-selector-button {
margin-inline-end: var(--_padding-inline);
}
/* special dark theme styles */
@media (prefers-color-scheme: dark) {
:where(
[type="submit"],
[type="reset"],
[disabled],
form button:not([type="button"])
) {
--_bg: var(--_input-well);
}
}
This pattern shows how to style the different buttons types of the web.
Full article · Video on YouTube · Source on Github
Carousel
<div class="gui-carousel" carousel-pagination="dots" carousel-controls="auto" carousel-scrollbar="auto"
carousel-snapstop="auto" aria-label="Featured Items Carousel">
<div class="gui-carousel--scroller">
<div class="gui-carousel--snap">
<figure class="animate-visibility captioned-image">
<img loading="lazy" width="1280" height="720" src="https://picsum.photos/seed/this/1280/720.webp"
alt="Blue ocean with a large wave">
<figcaption>
<a href="#">Learn more about large ocean waves</a>
</figcaption>
</figure>
</div>
<div class="gui-carousel--snap">
<figure class="animate-visibility captioned-image">
<img loading="lazy" width="1280" height="720" src="https://picsum.photos/seed/is/1280/720.webp"
alt="Frosty orange desert sunset">
<figcaption>
<a href="#">Learn more about warm deserts</a>
</figcaption>
</figure>
</div>
<div class="gui-carousel--snap">
<figure class="animate-visibility captioned-image">
<img loading="lazy" width="1280" height="720" src="https://picsum.photos/seed/a/1280/720.webp"
alt="African sahara with a giraffe">
<figcaption>
<a href="#">Learn more about giraffe's</a>
</figcaption>
</figure>
</div>
</div>
</div>
import {scrollend} from 'https://cdn.jsdelivr.net/gh/argyleink/scrollyfills@latest/dist/scrollyfills.modern.js'
export default class Carousel {
constructor(element) {
this.elements = {
root: element,
scroller: element.querySelector('.gui-carousel--scroller'),
snaps: element.querySelectorAll('.gui-carousel--snap'),
previous: null, // generated in #createControl
next: null, // generated in #createControl
pagination: null, // generated in #createPagination
}
this.current = undefined // set in #initializeState
this.hasIntersected = new Set() // holds intersection results used on scrollend
this.elements.root.setAttribute('tabindex', -1)
this.elements.root.setAttribute('aria-roledescription', 'carousel')
this.elements.scroller.setAttribute('role', 'group')
this.elements.scroller.setAttribute('aria-label', 'Items Scroller')
this.elements.scroller.setAttribute('aria-live', 'Polite')
this.#createObservers()
this.#createPagination()
this.#createControls()
this.#initializeState()
this.#listen()
this.#synchronize()
}
#synchronize() {
for (let observation of this.hasIntersected) {
// toggle inert when it's not intersecting
observation.target
.toggleAttribute('inert', !observation.isIntersecting)
// toggle aria-selected on pagination dots
const dot = this.elements.pagination
.children[this.#getElementIndex(observation.target)]
dot.setAttribute('aria-selected', observation.isIntersecting)
dot.setAttribute('tabindex', !observation.isIntersecting ? '-1' : '0')
// stash the intersecting snap element
if (observation.isIntersecting) {
this.current = observation.target
this.goToElement({
scrollport: this.elements.pagination,
element: dot,
})
}
}
this.#updateControls()
this.hasIntersected.clear()
}
goNext() {
const next = this.current.nextElementSibling
if (this.current === next)
return
if (next) {
this.goToElement({
scrollport: this.elements.scroller,
element: next,
})
this.current = next
}
else {
console.log('at the end')
}
}
goPrevious() {
const previous = this.current.previousElementSibling
if (this.current === previous)
return
if (previous) {
this.goToElement({
scrollport: this.elements.scroller,
element: previous,
})
this.current = previous
}
else {
console.log('at the beginning')
}
}
goToElement({scrollport, element}) {
const dir = this.#documentDirection()
const delta = Math.abs(scrollport.offsetLeft - element.offsetLeft)
const scrollerPadding = parseInt(getComputedStyle(scrollport)['padding-left'])
const pos = scrollport.clientWidth / 2 > delta
? delta - scrollerPadding
: delta + scrollerPadding
scrollport.scrollTo(dir === 'ltr' ? pos : pos*-1, 0)
}
#updateControls() {
const {lastElementChild:last, firstElementChild:first} = this.elements.scroller
const isAtEnd = this.current === last
const isAtStart = this.current === first
// before we possibly disable a button
// shift the focus to the complimentary button
if (document.activeElement === this.elements.next && isAtEnd)
this.elements.previous.focus()
else if (document.activeElement === this.elements.previous && isAtStart)
this.elements.next.focus()
this.elements.next.toggleAttribute('disabled', isAtEnd)
this.elements.previous.toggleAttribute('disabled', isAtStart)
}
#listen() {
// observe children intersection
for (let item of this.elements.snaps)
this.carousel_observer.observe(item)
// watch document for removal of this carousel node
this.mutation_observer.observe(document, {
childList: true,
subtree: true,
})
// scrollend listener for sync
this.elements.scroller.addEventListener('scrollend', this.#synchronize.bind(this))
this.elements.next.addEventListener('click', this.goNext.bind(this))
this.elements.previous.addEventListener('click', this.goPrevious.bind(this))
this.elements.pagination.addEventListener('click', this.#handlePaginate.bind(this))
this.elements.root.addEventListener('keydown', this.#handleKeydown.bind(this))
}
#unlisten() {
for (let item of this.elements.snaps)
this.carousel_observer.unobserve(item)
this.mutation_observer.disconnect()
this.elements.scroller.removeEventListener('scrollend', this.#synchronize)
this.elements.next.removeEventListener('click', this.goNext)
this.elements.previous.removeEventListener('click', this.goPrevious)
this.elements.pagination.removeEventListener('click', this.#handlePaginate)
this.elements.root.removeEventListener('keydown', this.#handleKeydown)
}
#createObservers() {
this.carousel_observer = new IntersectionObserver(observations => {
for (let observation of observations) {
this.hasIntersected.add(observation)
// toggle --in-view class if intersecting or not
observation.target.classList
.toggle('--in-view', observation.isIntersecting)
}
}, {
root: this.elements.scroller,
threshold: .6,
})
this.mutation_observer = new MutationObserver((mutationList, observer) => {
mutationList
.filter(x => x.removedNodes.length > 0)
.forEach(mutation => {
[...mutation.removedNodes]
.filter(x => x.querySelector('.gui-carousel') === this.elements.root)
.forEach(removedEl => {
this.#unlisten()
})
})
})
}
#initializeState() {
const startIndex = this.elements.root.hasAttribute('carousel-start')
? this.elements.root.getAttribute('carousel-start') - 1
: 0
this.current = this.elements.snaps[startIndex]
this.#handleScrollStart()
// each snap target needs a marker for pagination
// each snap needs some a11y love
this.elements.snaps.forEach((snapChild, index) => {
this.hasIntersected.add({
isIntersecting: index === 0,
target: snapChild,
})
this.elements.pagination
.appendChild(this.#createMarker(snapChild, index))
snapChild.setAttribute('aria-label', `${index+1} of ${this.elements.snaps.length}`)
snapChild.setAttribute('aria-roledescription', 'item')
})
}
#handleScrollStart() {
if (this.elements.root.hasAttribute('carousel-start')) {
const itemIndex = this.elements.root.getAttribute('carousel-start')
const startElement = this.elements.snaps[itemIndex - 1]
this.elements.snaps.forEach(snap =>
snap.style.scrollSnapAlign = 'unset')
startElement.style.scrollSnapAlign = null
startElement.style.animation = 'carousel-scrollstart 1ms'
startElement.addEventListener('animationend', e => {
startElement.animation = null
this.elements.snaps.forEach(snap =>
snap.style.scrollSnapAlign = null)
}, {once: true})
}
}
#handlePaginate(e) {
if (e.target.classList.contains('gui-carousel--pagination'))
return
e.target.setAttribute('aria-selected', true)
const item = this.elements.snaps[this.#getElementIndex(e.target)]
this.goToElement({
scrollport: this.elements.scroller,
element: item,
})
}
#handleKeydown(e) {
const dir = this.#documentDirection()
const idx = this.#getElementIndex(e.target)
switch (e.key) {
case 'ArrowRight':
e.preventDefault()
const next_offset = dir === 'ltr' ? 1 : -1
const next_control = dir === 'ltr' ? this.elements.next : this.elements.previous
if (e.target.closest('.gui-carousel--pagination'))
this.elements
.pagination.children[idx + next_offset]
?.focus()
else {
if (document.activeElement === next_control)
this.#keypressAnimation(next_control)
next_control.focus()
}
dir === 'ltr' ? this.goNext() : this.goPrevious()
break
case 'ArrowLeft':
e.preventDefault()
const previous_offset = dir === 'ltr' ? -1 : 1
const previous_control = dir === 'ltr' ? this.elements.previous : this.elements.next
if (e.target.closest('.gui-carousel--pagination'))
this.elements
.pagination.children[idx + previous_offset]
?.focus()
else {
if (document.activeElement === previous_control)
this.#keypressAnimation(previous_control)
previous_control.focus()
}
dir === 'ltr' ? this.goPrevious() : this.goNext()
break
}
}
#getElementIndex(element) {
let index = 0
while (element = element.previousElementSibling)
index++
return index
}
#createPagination() {
let nav = document.createElement('nav')
nav.className = 'gui-carousel--pagination'
this.elements.root.appendChild(nav)
this.elements.pagination = nav
}
#createMarker(item, index) {
const markerType = this.elements.root.getAttribute('carousel-pagination')
index++ // user facing index shouldnt start at 0
if (markerType == 'gallery')
return this.#createMarkerGallery({index, type: markerType, item})
else
return this.#createMarkerDot({index, type: markerType, item})
}
#createMarkerDot({index, type, item}) {
const marker = document.createElement('button')
const img = item.querySelector('img')
const caption = item.querySelector('figcaption')
marker.className = 'gui-carousel--control'
marker.type = 'button'
marker.role = 'tab'
marker.title = `Item ${index}: ${img?.alt || caption?.innerText}`
marker.setAttribute('aria-label', img?.alt || caption?.innerText)
marker.setAttribute('aria-setsize', this.elements.snaps.length)
marker.setAttribute('aria-posinset', index)
marker.setAttribute('aria-controls', `carousel-item-${index}`)
return marker
}
#createMarkerGallery({index, type, item}) {
const marker = document.createElement('button')
const img = item.querySelector('img')
marker.style.backgroundImage = `url(${img.src})`
marker.className = 'gui-carousel--control --gallery'
marker.type = 'button'
marker.role = 'tab'
marker.title = `Item ${index}: ${img.alt}`
marker.setAttribute('aria-label', img.alt)
marker.setAttribute('aria-setsize', this.elements.snaps.length)
marker.setAttribute('aria-posinset', index)
marker.setAttribute('aria-controls', `carousel-item-${index}`)
return marker
}
#createControls() {
let controls = document.createElement('div')
controls.className = 'gui-carousel--controls'
let prevBtn = this.#createControl('previous')
let nextBtn = this.#createControl('next')
controls.appendChild(prevBtn)
controls.appendChild(nextBtn)
this.elements.previous = prevBtn
this.elements.next = nextBtn
this.elements.root.prepend(controls)
}
#createControl(btnType) {
let control = document.createElement('button')
let userFacingText = `${btnType.charAt(0).toUpperCase() + btnType.slice(1)} Item`
control.type = 'button'
control.title = userFacingText
control.className = `gui-carousel--control --${btnType}`
control.setAttribute('aria-controls', 'gui-carousel--controls')
control.setAttribute('aria-label', userFacingText)
let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
svg.setAttribute('aria-hidden', 'true')
svg.setAttribute('viewBox', '0 0 20 20')
svg.setAttribute('fill', 'currentColor')
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
path.setAttribute('fill-rule', 'evenodd')
path.setAttribute('clip-rule', 'evenodd')
let previousPath = 'M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z'
let nextPath = 'M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z'
path.setAttribute('d', btnType === 'next' ? nextPath : previousPath)
svg.appendChild(path)
control.appendChild(svg)
return control
}
#keypressAnimation(element) {
element.style.animation = 'gui-carousel--control-keypress 145ms var(--ease-2)'
element.addEventListener('animationend', e => {
element.style.animation = null
}, {once: true})
}
#documentDirection() {
return document.firstElementChild.getAttribute('dir') || 'ltr'
}
}
document.querySelectorAll('.gui-carousel').forEach(element => {
new Carousel(element)
})
:where(.gui-carousel) {
--_carousel-item-size: 80%;
--_carousel-gutters: max(4rem, calc((100% - var(--_carousel-item-size)) / 2));
--_carousel-scrollbar-gutter: var(--size-6);
--_carousel-pagination-size: var(--size-8);
display: grid;
grid-template-columns: [carousel-gutter] var(--_carousel-gutters) 1fr [carousel-gutter] var(--_carousel-gutters);
grid-template-rows:
[carousel-scroller] 1fr
[carousel-pagination] var(--_carousel-pagination-size);
&:focus-visible {
outline-offset: -5px;
}
/* configuration handlers */
&[carousel-pagination="gallery"] {
--_carousel-pagination-size: var(--size-10);
& > .gui-carousel--pagination {
-webkit-mask-image: linear-gradient(to right, #0000 0%, #000 5%, 95%, #000, #0000);
}
}
&[carousel-pagination="none"] {
grid-template-rows: [carousel-scroller] 1fr;
& > .gui-carousel--pagination {
display: none;
}
}
&[carousel-controls="none"] {
grid-template-columns: 0 1fr 0;
& > .gui-carousel--controls {
display: none;
}
}
&[carousel-scrollbar="none"] {
--_carousel-pagination-size: var(--size-5);
& > .gui-carousel--scroller {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
& > .gui-carousel--pagination {
place-self: start center;
}
}
&[carousel-snapstop="always"] .gui-carousel--snap {
scroll-snap-stop: always;
}
}
:where(.gui-carousel--scroller) {
grid-row: 1;
grid-column: 1/-1;
display: grid;
grid-auto-columns: 100%;
grid-auto-flow: column;
align-items: center;
gap: var(--_carousel-gutters);
padding-block: var(--size-2) var(--_carousel-scrollbar-gutter);
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
scroll-padding-inline: var(--_carousel-gutters);
padding-inline: var(--_carousel-gutters);
@media (--motionOK) {
scroll-behavior: smooth;
}
}
:where(.gui-carousel--snap) {
scroll-snap-align: center;
}
:where(.gui-carousel--controls) {
display: flex;
justify-content: space-between;
padding-inline: var(--_carousel-gutters);
display: contents;
& > .gui-carousel--control {
margin-block-end: var(--_carousel-scrollbar-gutter);
&:not([disabled="true"]):active {
transform: scale(.95);
}
}
}
:where(.gui-carousel--control) {
--_shadow-size: 0;
--_shadow-highlight-light: hsl(0 0% 50% / 10%);
--_shadow-highlight-dark: hsl(0 0% 100% / 20%);
--_shadow-highlight: var(--_shadow-highlight-light);
grid-row: 1;
place-self: center;
background: var(--surface-1);
color: var(--text-2);
inline-size: var(--size-8);
aspect-ratio: var(--ratio-square);
border-radius: var(--radius-round);
box-shadow: 0 0 0 var(--_shadow-size) var(--_shadow-highlight);
border: var(--border-size-1) solid transparent;
text-indent: 10ch;
padding: 0;
overflow: hidden;
z-index: var(--layer-1);
transition: opacity .5s var(--ease-2) .5s;
@media (--motionOK) {
transition:
opacity .5s var(--ease-2) .5s,
transform .2s var(--ease-4),
box-shadow .2s var(--ease-4),
outline-offset 145ms var(--ease-2)
;
}
@media (--OSdark) {
--_shadow-highlight: var(--_shadow-highlight-dark);
}
&:hover {
--_shadow-size: 6px;
}
&.--previous {
grid-column: 1;
}
&.--next {
grid-column: 3;
}
@nest [dir="rtl"] & > svg {
transform: rotateY(180deg);
}
&[disabled] {
cursor: not-allowed;
transition-delay: 0s;
& > svg {
opacity: .25;
}
}
&:not([disabled]):is(:hover, :focus-visible) {
color: var(--link);
}
&:not([disabled]) svg > path {
@media (--motionOK) {
--_transform: translateX(var(--_x)) scale(.95);
transition: transform .5s var(--ease-squish-3);
transform-origin: center center;
}
}
&[aria-label="Next Item"]:not([disabled]):is(:hover, :focus-visible) svg > path {
--_x: 2px;
transform: var(--_transform);
}
&[aria-label="Previous Item"]:not([disabled]):is(:hover, :focus-visible) svg > path {
--_x: -2px;
transform: var(--_transform);
}
}
:where(.gui-carousel--pagination) {
grid-column: 1/-1;
place-self: center;
display: grid;
grid-auto-flow: column;
gap: var(--size-2);
max-inline-size: 100%;
overflow-x: auto;
overscroll-behavior-x: contain;
padding-block: var(--size-2);
padding-inline: var(--size-4);
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
@media (--motionOK) {
scroll-behavior: smooth;
}
@nest [carousel-pagination="gallery"] & {
margin-block-end: 0;
}
& > [aria-selected="true"] {
background: var(--link);
}
& > [aria-selected="false"] {
transform: scale(.75);
}
& > button {
inline-size: var(--size-3);
background-color: var(--surface-4);
border: var(--border-size-1) solid transparent;
&.--gallery {
inline-size: var(--size-fluid-5);
border-radius: var(--radius-2);
border: none;
background-origin: border-box;
background-size: cover;
}
}
}
@keyframes gui-carousel--control-keypress {
0% { outline-offset: 5px }
50% { outline-offset: 0; }
}
@keyframes carousel-scrollstart {
from { scroll-snap-align: center }
to { scroll-snap-align: unset }
}
This pattern shows how to create color-adaptive, responsive, and accessible mini and mega modals with the dialog element.
Video on YouTube · Source on Github
Dialog
<dialog id="MegaDialog" inert loading modal-mode="mega">
<form method="dialog">
<header>
<section class="icon-headline">
<svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="8.5" cy="7" r="4"></circle>
<line x1="20" y1="8" x2="20" y2="14"></line>
<line x1="23" y1="11" x2="17" y2="11"></line>
</svg>
<h3>New User</h3>
</section>
<button onclick="this.closest('dialog').close('close')" type="button" title="Close dialog">
<title>Close dialog icon</title>
<svg width="24" height="24" viewBox="0 0 24 24">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</header>
<article>
<section class="labelled-input">
<label for="userimage">Upload an image</label>
<input id="userimage" name="userimage" type="file">
</section>
<small><b>*</b> Maximum upload 1mb</small>
</article>
<footer>
<menu>
<button type="reset" value="clear">Clear</button>
</menu>
<menu>
<button autofocus type="button" onclick="this.closest('dialog').close('cancel')">Cancel</button>
<button type="submit" value="confirm">Confirm</button>
</menu>
</footer>
</form>
</dialog>
<dialog id="MiniDialog" inert loading modal-mode="mini">
<form method="dialog">
<article>
<section class="warning-message">
<svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24" >
<title>A warning icon</title>
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
<p>Are you sure you want to remove this user?</p>
</section>
</article>
<footer>
<menu>
<button autofocus type="button" onclick="this.closest('dialog').close('cancel')">Cancel</button>
<button type="submit" value="confirm">Confirm</button>
</menu>
</footer>
</form>
</dialog>
// custom events to be added to <dialog>
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent = new Event('closed')
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent = new Event('opened')
const dialogRemovedEvent = new Event('removed')
// track opening
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(async mutation => {
if (mutation.attributeName === 'open') {
const dialog = mutation.target
const isOpen = dialog.hasAttribute('open')
if (!isOpen) return
dialog.removeAttribute('inert')
// set focus
const focusTarget = dialog.querySelector('[autofocus]')
focusTarget
? focusTarget.focus()
: dialog.querySelector('button').focus()
dialog.dispatchEvent(dialogOpeningEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogOpenedEvent)
}
})
})
// track deletion
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(mutation => {
mutation.removedNodes.forEach(removedNode => {
if (removedNode.nodeName === 'DIALOG') {
removedNode.removeEventListener('click', lightDismiss)
removedNode.removeEventListener('close', dialogClose)
removedNode.dispatchEvent(dialogRemovedEvent)
}
})
})
})
// wait for all dialog animations to complete their promises
const animationsComplete = element =>
Promise.allSettled(
element.getAnimations().map(animation =>
animation.finished))
// click outside the dialog handler
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
const dialogClose = async ({target:dialog}) => {
dialog.setAttribute('inert', '')
dialog.dispatchEvent(dialogClosingEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogClosedEvent)
}
// page load dialogs setup
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
dialog.addEventListener('close', dialogClose)
dialogAttrObserver.observe(dialog, {
attributes: true,
})
dialogDeleteObserver.observe(document.body, {
attributes: false,
subtree: false,
childList: true,
})
// remove loading attribute
// prevent page load @keyframes playing
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
@import "https://unpkg.com/open-props";
@import "https://unpkg.com/open-props/normalize.min.css";
html:has(dialog[open][modal-mode="mega"]) {
overflow: hidden;
}
dialog {
display: grid;
background: var(--surface-2);
color: var(--text-1);
max-inline-size: min(90vw, var(--size-content-3));
max-block-size: min(80vh, 100%);
max-block-size: min(80dvb, 100%);
margin: auto;
padding: 0;
position: fixed;
inset: 0;
border-radius: var(--radius-3);
box-shadow: var(--shadow-6);
z-index: var(--layer-important);
overflow: hidden;
transition: opacity .5s var(--ease-3);
@media (--motionOK) {
animation: var(--animation-scale-down) forwards;
animation-timing-function: var(--ease-squish-3);
}
@media (--OSdark) {
border-block-start: var(--border-size-1) solid var(--surface-3);
}
@media (--md-n-below) {
&[modal-mode="mega"] {
margin-block-end: 0;
border-end-end-radius: 0;
border-end-start-radius: 0;
@media (--motionOK) {
animation: var(--animation-slide-out-down) forwards;
animation-timing-function: var(--ease-squish-2);
}
}
}
&:not([open]) {
pointer-events: none;
opacity: 0;
}
&[modal-mode="mega"]::backdrop {
backdrop-filter: blur(25px);
}
&[modal-mode="mini"]::backdrop {
backdrop-filter: none;
}
&::backdrop {
transition: backdrop-filter .5s ease;
}
&[loading] {
visibility: hidden;
}
&[open] {
@media (--motionOK) {
animation: var(--animation-slide-in-up) forwards;
}
}
& > form {
display: grid;
grid-template-rows: auto 1fr auto;
align-items: start;
max-block-size: 80vh;
max-block-size: 80dvb;
& > article {
overflow-y: auto;
max-block-size: 100%; /* safari */
overscroll-behavior-y: contain;
display: grid;
justify-items: flex-start;
gap: var(--size-3);
box-shadow: var(--shadow-2);
z-index: var(--layer-1);
padding-inline: var(--size-5);
padding-block: var(--size-3);
@media (--OSlight) {
background: var(--surface-1);
&::-webkit-scrollbar {
background: var(--surface-1);
}
}
@media (--OSdark) {
border-block-start: var(--border-size-1) solid var(--surface-3);
}
}
& > header {
display: flex;
gap: var(--size-3);
justify-content: space-between;
align-items: flex-start;
padding-block: var(--size-3);
padding-inline: var(--size-5);
& > button {
border-radius: var(--radius-round);
padding: .75ch;
aspect-ratio: 1;
flex-shrink: 0;
place-items: center;
stroke: currentColor;
stroke-width: 3px;
}
}
& > footer {
display: flex;
flex-wrap: wrap;
gap: var(--size-3);
justify-content: space-between;
align-items: flex-start;
padding-inline: var(--size-5);
padding-block: var(--size-3);
& > menu {
display: flex;
flex-wrap: wrap;
gap: var(--size-3);
padding-inline-start: 0;
&:only-child {
margin-inline-start: auto;
}
@media (max-width: 410px) {
& button[type="reset"] {
display: none;
}
}
}
}
& > :is(header, footer) {
background-color: var(--surface-2);
@media (--OSdark) {
background-color: var(--surface-1);
}
}
}
}
This pattern shows how to create color-adaptive, responsive, and accessible mini and mega modals with the dialog element.
Full article · Video on YouTube · Source on Github
Game Menu
<ul class="threeD-button-set">
<li><button>New Game</button></li>
<li><button>Continue</button></li>
<li><button>Online</button></li>
<li><button>Settings</button></li>
<li><button>Quit</button></li>
</ul>
import {rovingIndex} from 'https://cdn.skypack.dev/roving-ux'
const menu = document.querySelector('.threeD-button-set')
const menuRect = menu.getBoundingClientRect()
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
rovingIndex({
element: document.querySelector('.threeD-button-set'),
target: 'button',
})
if (motionOK) {
window.addEventListener('mousemove', ({target, clientX, clientY}) => {
const {dx,dy} = getAngles(clientX, clientY)
menu.style.setProperty('--x', `${dy / 20}deg`)
menu.style.setProperty('--y', `${dx / 20}deg`)
})
}
const getAngles = (clientX, clientY) => {
const { x, y, width, height } = menuRect
const dx = clientX - (x + 0.5 * width)
const dy = clientY - (y + 0.5 * height)
return {dx,dy}
}
body {
perspective: 40vw;
}
.threeD-button-set {
--y:;
--x:;
--distance: 1px;
--theme: hsl(180 100% 50%);
--theme-bg: hsl(180 100% 50% / 25%);
--theme-bg-hover: hsl(180 100% 50% / 40%);
--theme-text: white;
--theme-shadow: hsl(180 100% 10% / 25%);
--_max-rotateY: 10deg;
--_max-rotateX: 15deg;
--_btn-bg: var(--theme-bg);
--_btn-bg-hover: var(--theme-bg-hover);
--_btn-text: var(--theme-text);
--_btn-text-shadow: var(--theme-shadow);
--_bounce-ease: cubic-bezier(.5, 1.75, .75, 1.25);
/* remove <ul> margins */
margin: 0;
/* vertical rag-right layout */
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2.5vh;
/* create 3D space context */
transform-style: preserve-3d;
/* clamped menu rotation to not be too extreme */
transform:
rotateY(
clamp(
calc(var(--_max-rotateY) * -1),
var(--y),
var(--_max-rotateY)
)
)
rotateX(
clamp(
calc(var(--_max-rotateX) * -1),
var(--x),
var(--_max-rotateX)
)
)
;
/* removes Safari focus ring on <ul> after button interaction */
&:focus {
outline: none;
}
@media (--motionOK) {
will-change: transform;
transition: transform .1s ease;
animation: rotate-y 5s ease-in-out infinite;
}
@media (--dark) {
--theme: hsl(255 53% 50%);
--theme-bg: hsl(255 53% 71% / 25%);
--theme-bg-hover: hsl(255 53% 50% / 40%);
--theme-shadow: hsl(255 53% 10% / 25%);
}
@media (--HDcolor) {
@supports (color: color(display-p3 0 0 0)) {
--theme: color(display-p3 .4 0 .9);
}
}
}
.threeD-button-set > li {
/* change display type from list-item */
display: inline-flex;
/* create context for button pseudos */
position: relative;
/* create 3D space context */
transform-style: preserve-3d;
}
.threeD-button-set button {
/* strip out default button styles */
appearance: none;
outline: none;
border: none;
-webkit-tap-highlight-color: transparent;
/* bring in brand styles via props */
background-color: var(--_btn-bg);
color: var(--_btn-text);
text-shadow: 0 1px 1px var(--_btn-text-shadow);
font-size: min(5vmin, 3rem);
font-family: Audiowide;
padding-block: .75ch;
padding-inline: 2ch;
border-radius: 5px 20px;
/* prepare for 3D perspective transforms */
transform: translateZ(var(--distance));
transform-style: preserve-3d;
&:is(:hover, :focus-visible):not(:active) {
/* subtle distance plus bg color change on hover/focus */
--distance: 15px;
background-color: var(--_btn-bg-hover);
/* if motion is OK, setup transitions and increase distance */
@media (--motionOK) {
--distance: 3vmax;
transition-timing-function: var(--_bounce-ease);
transition-duration: .4s;
&::after { transition-duration: .5s }
&::before { transition-duration: .3s }
}
}
&::after,
&::before {
/* create empty element */
content: '';
opacity: .8;
/* cover the parent (button) */
position: absolute;
inset: 0;
/* style the element for border accents */
border: 1px solid var(--theme);
border-radius: 5px 20px;
/* move in Z space with a multiplier */
transform: translateZ(calc(var(--distance) / 3));
/* if motion is OK, transition the Z space move */
@media (--motionOK) {
transition: transform .1s ease-out;
}
}
/* exceptions for one of the pseudo elements */
/* this will be pushed back and have a thicker border */
&::before {
border-width: 3px;
transform: translateZ(calc(var(--distance) / 3 * -1));
/* in dark mode, it glows! */
@media (--dark) {
box-shadow:
0 0 25px var(--theme),
inset 0 0 25px var(--theme);
}
}
@media (--motionOK) {
will-change: transform;
transition:
transform .2s ease,
background-color .5s ease;
}
}
@keyframes rotate-y {
50% {
transform: rotateY(15deg) rotateX(-6deg);
}
}
This pattern shows how to create a mouse and keyboard accessible 3D game menu.
Note: this component uses a library called roving-ux to create a focus group.
Full article · Video on YouTube · Source on Github
Loading Bar
<main id="loading-zone" aria-busy="true">
<p>Loading Level</p>
<div class="card">
<label>
<span class="sr-only">Loading progress</span>
<progress
indeterminate
role="progressbar"
aria-describedby="loading-zone"
tabindex="-1"
>unknown</progress>
</label>
</div>
</main>
const progress = document.querySelector('progress')
const zone = document.querySelector('#loading-zone')
const state = {
val: .1
}
const roundDecimals = (val, places) =>
+(Math.round(val + "e+" + places) + "e-" + places)
const setProgress = () => {
// set loading zone status
zone.setAttribute('aria-busy', state.val < 1)
// clear attributes if no value to show
// <progress> will show indeterminate state
if (state.val === null) {
progress.removeAttribute('aria-valuenow')
progress.removeAttribute('value')
progress.focus()
return
}
// round bad JS decimal math
const val = roundDecimals(state.val, 2)
const valPercent = val * 100 + "%"
// set value for screenreaders and element values
progress.value = val
progress.setAttribute('aria-valuenow', valPercent)
progress.innerText = valPercent
// focus so screenreaders hear the announced value update
progress.focus()
}
progress {
--_track: hsl(228 100% 90%);
--_track-size: min(10px, 1ex);
--_progress: hsl(228 100% 50%);
--_radius: 1e3px;
--_indeterminate-track: linear-gradient(to right,
var(--_track) 45%,
var(--_progress) 0%,
var(--_progress) 55%,
var(--_track) 0%
);
--_indeterminate-track-size: 225% 100%;
--_indeterminate-track-animation: progress-loading 2s infinite ease;
/* reset */
appearance: none;
border: none;
/* custom style */
position: relative;
height: var(--_track-size);
border-radius: var(--_radius);
overflow: hidden;
@media (prefers-color-scheme: dark) {
--_track: hsl(228 20% 30%);
--_progress: hsl(228 100% 75%);
}
&:focus-visible {
outline-color: var(--_progress);
}
/* Safari/Chromium */
&[value]::-webkit-progress-bar {
background-color: var(--_track);
}
&[value]::-webkit-progress-value {
background-color: var(--_progress);
transition: inline-size .25s ease-out;
}
/* Firefox */
&[value]::-moz-progress-bar {
background-color: var(--_progress);
}
/* indeterminate */
&:indeterminate::after {
content: "";
inset: 0;
position: absolute;
background: var(--_indeterminate-track);
background-size: var(--_indeterminate-track-size);
background-position: right;
animation: var(--_indeterminate-track-animation);
}
/* indeterminate Safari */
&:indeterminate::-webkit-progress-bar {
background: var(--_indeterminate-track);
background-size: var(--_indeterminate-track-size);
background-position: right;
animation: var(--_indeterminate-track-animation);
}
/* indeterminate Firefox */
&:indeterminate::-moz-progress-bar {
background: var(--_indeterminate-track);
background-size: var(--_indeterminate-track-size);
background-position: right;
animation: var(--_indeterminate-track-animation);
}
/* complete */
&:not([max])[value="1"]::before,
&[max="100"][value="100"]::before {
content: "✓";
position: absolute;
inset-block: 0;
inset-inline: auto 0;
display: flex;
align-items: center;
padding-inline-end: max(calc(var(--_track-size) / 4), 3px);
color: white;
font-size: calc(var(--_track-size) / 1.25);
}
}
@keyframes progress-loading {
50% {
background-position: left;
}
}
This pattern shows how to build a color adaptive and accessible loading bar with the progress element.
Full article · Video on YouTube · Source on Github
Media Scroller
<section>
<header>
<h2>Similar to Locke & Key</h2>
<h3>Popular with similar viewers</h3>
</header>
<ul class="horizontal-media-scroller">
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?179">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?82">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?39">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?94">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?95">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?86">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?87">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?88">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?79">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?910">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?198">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?287">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?397">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?789">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?785">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?76">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?78">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?788">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?997">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?170">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?18">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?27">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?93">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?48">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?75">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?76">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?79">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?78">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?999">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?170">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?13">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?25">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?322">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?43">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?01">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?02">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?03">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?04">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?05">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?06">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?07">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?08">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?09">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?010">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?01">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?02">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?03">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?04">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?05">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?06">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?07">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?08">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?09">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?010">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?1">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?2">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?3">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?4">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?5">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?6">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?7">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?8">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?9">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?10">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?01">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?02">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?03">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?04">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?05">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?06">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?07">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?08">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?09">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?010">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
</ul>
</section>
<section>
<header>
<h2>Suspense</h2>
</header>
<ul class="horizontal-media-scroller">
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?14">
</picture>
<figcaption>The Strain (limited)</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?15">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?16">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?17">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?18">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?19">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?11">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?12">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?13">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?110">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?121">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?132">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?123">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?144">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?125">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?126">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?137">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?118">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?419">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?110">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?112">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?122">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?313">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?194">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?915">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?176">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?187">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?178">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?189">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?117">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
</ul>
</section>
<section>
<header>
<h2>Horror</h2>
</header>
<ul class="horizontal-media-scroller">
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?26">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?27">
</picture>
<figcaption>Penny Awful: An original series</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?28">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?29">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?210">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?21">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?22">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?23">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?24">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?25">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?26">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?27">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?28">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?29">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?210">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?21">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?21">
</picture>
<figcaption>Legends</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?22">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?23">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?24">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?25">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?22">
</picture>
<figcaption>The Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?23">
</picture>
<figcaption>Almost Family</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?24">
</picture>
<figcaption>The Strain</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?25">
</picture>
<figcaption>The Following</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?26">
</picture>
<figcaption>BERLIN STATION</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?27">
</picture>
<figcaption>Penny Dreadful</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?28">
</picture>
<figcaption>Castle Rock</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?29">
</picture>
<figcaption>The Walking Dead</figcaption>
</figure></a></li>
<li><a href="#"><figure>
<picture>
<img alt="A placeholder image" loading="lazy" src="https://picsum.photos/500/500?210">
</picture>
<figcaption>Fear The Walking Dead</figcaption>
</figure></a></li>
</ul>
</section>
import {rovingIndex} from "https://cdn.skypack.dev/roving-ux"
document.querySelectorAll('.horizontal-media-scroller')
.forEach(scroller => rovingIndex({
element: scroller,
target: 'a',
}))
.horizontal-media-scroller {
--size: 150px;
display: grid;
grid-auto-flow: column;
gap: calc(var(--gap) / 2);
margin: 0;
padding-inline-start: var(--gap);
padding-inline-end: var(--gap);
padding-block-start: calc(var(--gap) / 2);
padding-block-end: calc(var(--gap) / 2);
overflow-x: auto;
overscroll-behavior-inline: contain;
scroll-snap-type: inline mandatory;
scroll-padding-left: var(--gap);
scroll-padding-right: var(--gap);
scroll-padding-inline: var(--gap);
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
& > li {
display: inline-block;
/* container padding fix */
&:last-of-type figure {
position: relative;
&::after {
content: "";
position: absolute;
inline-size: var(--gap);
block-size: 100%;
inset-block-start: 0;
inset-inline-end: calc(var(--gap) * -1);
}
}
}
& figure {
scroll-snap-align: start;
}
& a {
display: inline-block;
text-decoration: none;
color: inherit;
outline-offset: 12px;
&:focus {
outline-offset: 7px;
}
@media (prefers-reduced-motion: no-preference) {
transition: outline-offset .25s ease;
}
}
}
This pattern shows how to create horizontal scrolling experiences for the web that are minimal, responsive, accessible and work across browsers and platforms (like TVs!)
Note: this component uses a library called roving-ux to create a focus group.
Full article · Video on YouTube · Source on Github
Multi-Select
<main>
<header>
<h1>Lighting</h1>
<small>Find your perfect light</small>
</header>
<aside>
<form>
<select multiple="true" title="Filter results by category">
<optgroup label="New">
<option value="last 30 days">Last 30 Days</option>
<option value="last 6 months">Last 6 Months</option>
</optgroup>
<optgroup label="Lamps">
<option value="table lamps">Table Lamps</option>
<option value="desk lamps">Desk Lamps</option>
<option value="floor lamps">Floor Lamps</option>
</optgroup>
<optgroup label="Ceiling">
<option value="chandeliers">Chandeliers</option>
<option value="pendant">Pendant</option>
<option value="flush">Flush</option>
<option value="fans">Fans</option>
</optgroup>
<optgroup label="By Room">
<option value="bedroom">Bedroom</option>
<option value="dining room">Dining Room</option>
<option value="kitchen">Kitchen</option>
<option value="living room">Living Room</option>
<option value="bathroom">Bathroom</option>
<option value="entryway">Entryway</option>
<option value="outdoor">Outdoor</option>
</optgroup>
<optgroup label="Kids">
<option value="lamps">Lamps</option>
<option value="night lights">Night Lights</option>
<option value="ceiling">Ceiling</option>
</optgroup>
</select>
<fieldset>
<legend>New</legend>
<div>
<input type="checkbox" id="last-30-days" name="new" value="last 30 days">
<label for="last-30-days">Last 30 Days</label>
</div>
<div>
<input type="checkbox" id="last-6-months" name="new" value="last 6 months">
<label for="last-6-months">Last 6 Months</label>
</div>
</fieldset>
<fieldset>
<legend>Lamps</legend>
<div>
<input type="checkbox" id="table-lamps" name="lamps" value="table lamps">
<label for="table-lamps">Table Lamps</label>
</div>
<div>
<input type="checkbox" id="desk-lamps" name="lamps" value="desk lamps">
<label for="desk-lamps">Desk Lamps</label>
</div>
<div>
<input type="checkbox" id="floor-lamps" name="lamps" value="floor lamps">
<label for="floor-lamps">Floor Lamps</label>
</div>
</fieldset>
<fieldset>
<legend>Ceiling</legend>
<div>
<input type="checkbox" id="chandeliers" name="ceiling" value="chandeliers">
<label for="chandeliers">Chandeliers</label>
</div>
<div>
<input type="checkbox" id="pendant" name="ceiling" value="pendant">
<label for="pendant">Pendant</label>
</div>
<div>
<input type="checkbox" id="flush" name="ceiling" value="flush">
<label for="flush">Flush</label>
</div>
<div>
<input type="checkbox" id="fans" name="ceiling" value="fans">
<label for="fans">Fans</label>
</div>
</fieldset>
<fieldset>
<legend>By Room</legend>
<div>
<input type="checkbox" id="bedroom" name="by room" value="bedroom">
<label for="bedroom">Bedroom</label>
</div>
<div>
<input type="checkbox" id="dining-room" name="by room" value="dining room">
<label for="dining-room">Dining Room</label>
</div>
<div>
<input type="checkbox" id="kitchen" name="by room" value="kitchen">
<label for="kitchen">Kitchen</label>
</div>
<div>
<input type="checkbox" id="living-room" name="by room" value="living room">
<label for="living-room">Living Room</label>
</div>
<div>
<input type="checkbox" id="bathroom" name="by room" value="bathroom">
<label for="bathroom">Bathroom</label>
</div>
<div>
<input type="checkbox" id="entryway" name="by room" value="entryway">
<label for="entryway">Entryway</label>
</div>
<div>
<input type="checkbox" id="outdoor" name="by room" value="outdoor">
<label for="outdoor">Outdoor</label>
</div>
</fieldset>
<fieldset>
<legend>Kids</legend>
<div>
<input type="checkbox" id="lamps" name="kids" value="lamps">
<label for="lamps">Lamps</label>
</div>
<div>
<input type="checkbox" id="night-lights" name="kids" value="night lights">
<label for="night-lights">Night Lights</label>
</div>
<div>
<input type="checkbox" id="ceiling" name="kids" value="ceiling">
<label for="ceiling">Ceiling</label>
</div>
</fieldset>
</form>
<div role="status" class="sr-only" id="applied-filters"></div>
</aside>
<article>
<span class="last-30-days table-lamps"></span>
<span class="last-6-months desk-lamps"></span>
<span class="floor-lamps"></span>
<span class="last-6-months chandeliers"></span>
<span class="pendant last-6-months"></span>
<span class="flush fans"></span>
<span class="fans pendant table-lamps"></span>
<span class="bedroom"></span>
<span class="dining-room last-30-days chandeliers"></span>
<span class="kitchen lamps"></span>
<span class="living-room"></span>
<span class="bathroom living-room chandeliers desk-lamps"></span>
<span class="bathroom table-lamps desk-lamps"></span>
<span class="entryway last-30-days"></span>
<span class="outdoor desk-lamps"></span>
<span class="lamps last-30-days"></span>
<span class="night-lights table-lamps"></span>
<span class="ceiling last-30-days"></span>
<span class="floor-lamps table-lamps"></span>
<span class="floor-lamps last-6-months"></span>
<span class="dining-room last-30-days chandeliers"></span>
<span class="kitchen lamps"></span>
<span class="living-room"></span>
<span class="bathroom living-room chandeliers desk-lamps"></span>
</article>
</main>
import 'https://unpkg.com/isotope-layout@3.0.6/dist/isotope.pkgd.min.js'
const IsotopeGrid = new Isotope( 'article', {
itemSelector: 'span',
layoutMode: 'fitRows',
percentPosition: true
})
const filterGrid = query => {
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
IsotopeGrid.arrange({
filter: query,
stagger: 25,
transitionDuration: motionOK ? '0.4s' : 0,
})
}
// takes a <select> and returns the selection as an array
const prepareSelectOptions = element =>
Array.from(element.selectedOptions).reduce((data, opt) => {
data.push([opt.parentElement.label.toLowerCase(), opt.value])
return data
}, [])
// <select> watcher
document.querySelector('select').addEventListener('input', e => {
let selectData = prepareSelectOptions(e.target)
console.warn('Multiselect', selectData)
// DEMO
// isotope query assembly from checkbox selections
let query = selectData.reduce((query, val) => {
query.push('.' + val[1].split(' ').join('-'))
return query
}, []).join(',')
filterGrid(query)
// update for assistive technology
let statusRoleElement = document.querySelector('#applied-filters')
let filterResults = IsotopeGrid.getFilteredItemElements().length
statusRoleElement.style.counterSet = selectData.length
statusRoleElement.textContent = " giving " + filterResults + " results"
})
document
.querySelector('aside form')
.addEventListener('input', e => {
if (e.target.nodeName === 'SELECT') return
const formData = new FormData(document.querySelector('form'))
console.warn('Checkboxes', Array.from(formData.entries()))
// DEMO
// isotope query assembly from checkbox selections
let query = Array.from(formData.values()).reduce((query, val) => {
query.push('.' + val.split(' ').join('-'))
return query
}, []).join(',')
filterGrid(query)
document.querySelector('#applied-filters').textContent = " giving " + IsotopeGrid.getFilteredItemElements().length + " results"
})
main {
display: grid;
grid-template-columns: max-content 1fr;
gap: 5vmin;
align-items: flex-start;
& > header {
grid-column: 1 / -1;
}
@media (orientation: portrait) {
grid-template-columns: 1fr;
}
@media (--useSelect) {
& > article {
grid-row: 3;
grid-column: 1 / -1;
}
}
}
article {
--size: min(300px, calc(25% - 2ch));
margin: -1ch;
& > span {
will-change: transform;
background: hsl(0 0% 50% / 25%);
border-radius: 10px;
inline-size: var(--size);
block-size: 15ch;
margin: 1ch;
@media (orientation: portrait) {
--size: calc(50% - 2ch);
}
@supports (aspect-ratio: 1) {
block-size: auto;
aspect-ratio: 1;
}
}
}
header {
display: grid;
gap: 1ch;
}
aside {
counter-reset: filters;
& :checked {
counter-increment: filters;
}
& #applied-filters::before {
content: counter(filters) " filters ";
}
}
fieldset:first-of-type {
margin-block-start: -5px;
}
[role="status"] {
@media (--useSelect) {
display: none;
}
}
.sr-only {
inline-size: 0;
block-size: 0;
overflow: hidden;
}
This pattern shows how to build a responsive, adaptive, and accessible, multiselect component for sort and filter user experiences.
Note: this component uses a library called Isotope to help with the animation of filtering and sorting in the layout.
Full article · Video on YouTube · Source on Github
Settings
<main>
<h1>Settings</h1>
<form>
<section>
<header>
<h2>Sound & vibration</h2>
<small>Adjust system volume channels</small>
</header>
<fieldset>
<div class="fieldset-item">
<picture aria-hidden="true">
<svg viewBox="0 0 24 24">
<title>A note icon</title>
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
</svg>
</picture>
<div class="input-stack">
<label
for="media-volume"
id="media-volume"
aria-hidden="true">
Media volume
</label>
<input
name="media-volume"
aria-labelledby="media-volume"
type="range"
value="3"
max="10"
style="--track-fill: 30%"
>
</div>
</div>
<div class="fieldset-item">
<picture aria-hidden="true">
<title>A phone icon</title>
<svg viewBox="0 0 24 24">
<path d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z"/>
</svg>
</picture>
<div class="input-stack">
<label for="call-volume" id="call-volume" aria-hidden="true">Call volume</label>
<input
name="call-volume"
aria-labelledby="call-volume"
type="range"
value="7"
max="10"
style="--track-fill: 70%"
>
</div>
</div>
<div class="fieldset-item">
<picture aria-hidden="true">
<svg viewBox="0 0 24 24">
<title>A bell icon</title>
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/>
</svg>
</picture>
<div class="input-stack">
<label for="ring-volume" id="ring-volume" aria-hidden="true">Ring volume</label>
<input
name="ring-volume"
aria-labelledby="ring-volume"
type="range"
value="5"
max="10"
style="--track-fill: 50%"
>
</div>
</div>
<div class="fieldset-item">
<picture aria-hidden="true">
<svg viewBox="0 0 24 24">
<title>An alarm clock icon</title>
<path d="M22 5.72l-4.6-3.86-1.29 1.53 4.6 3.86L22 5.72zM7.88 3.39L6.6 1.86 2 5.71l1.29 1.53 4.59-3.85zM12.5 8H11v6l4.75 2.85.75-1.23-4-2.37V8zM12 4c-4.97 0-9 4.03-9 9s4.02 9 9 9c4.97 0 9-4.03 9-9s-4.03-9-9-9zm0 16c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"/>
</svg>
</picture>
<div class="input-stack">
<label for="alarm-volume" id="alarm-volume" aria-hidden="true">Alarm volume</label>
<input
name="alarm-volume"
aria-labelledby="alarm-volume"
type="range"
value="8"
max="10"
style="--track-fill: 80%"
>
</div>
</div>
</fieldset>
</section>
<section>
<header>
<h2>Notifications</h2>
<small>Turn specific channels on/off</small>
</header>
<fieldset>
<div class="fieldset-item">
<input
type="checkbox"
checked
id="text-notifications"
name="text-notifications"
>
<div class="input-stack">
<label for="text-notifications">
<h3>Text Messages</h3>
<small>Get notified about all text messages sent to your device</small>
</label>
</div>
</div>
<div class="fieldset-item">
<input
type="checkbox"
id="voice-notifications"
name="voice-notifications"
>
<div class="input-stack">
<label for="voice-notifications">
<h3>Voice Mail</h3>
<small>Get notified about all voice messages sent to your device</small>
</label>
</div>
</div>
<div class="fieldset-item">
<input
type="checkbox"
id="email-notifications"
name="email-notifications"
>
<div class="input-stack">
<label for="email-notifications">
<h3>Emails</h3>
<small>Get notified about all text messages to your device</small>
</label>
</div>
</div>
</fieldset>
</section>
</form>
</main>
const form = document.querySelector('form')
const sliders = document.querySelectorAll('input[type="range"]')
const rangeToPercent = slider => {
const max = slider.getAttribute('max') || 10
const percent = slider.value / max * 100
return `${parseInt(percent)}%`
}
sliders.forEach(slider => {
slider.style.setProperty('--track-fill', rangeToPercent(slider))
slider.addEventListener('input', e => {
e.target.style.setProperty('--track-fill', rangeToPercent(e.target))
})
})
form.addEventListener('input', e => {
const formData = Object.fromEntries(new FormData(form))
console.table(formData)
})
@custom-media --motionOK (prefers-reduced-motion: no-preference);
:root {
--surface1: lch(10 0 0);
--surface2: lch(15 0 0);
--surface3: lch(20 0 0);
--surface4: lch(25 0 0);
--text1: lch(95 0 0);
--text2: lch(75 0 0);
--brand: lch(64 20 237);
--brand-bg1: lch(70 64 349);
--brand-bg2: lch(60 84 300);
--brand-bg-gradient: linear-gradient(
to bottom,
var(--brand-bg1),
var(--brand-bg2)
);
--thumb-highlight-color: lch(100 0 0 / 20%);
--space-xxs: .25rem;
--space-xs: .5rem;
--space-sm: 1rem;
--space-md: 1.5rem;
--space-lg: 2rem;
--space-xl: 3rem;
--space-xxl: 6rem;
--isLTR: 1;
--isRTL: -1;
&:dir(rtl) {
--isLTR: -1;
--isRTL: 1;
}
@media (prefers-color-scheme: light) {
& {
--surface1: lch(90 0 0);
--surface2: lch(100 0 0);
--surface3: lch(98 0 0);
--surface4: lch(85 0 0);
--text1: lch(20 0 0);
--text2: lch(40 0 0);
--brand: lch(64 40 237);
--brand-bg1: lch(50 64 349);
--brand-bg2: lch(40 84 300);
--thumb-highlight-color: lch(0 0 0 / 20%);
}
}
}
html {
block-size: 100%;
inline-size: 100%;
}
body {
min-block-size: 100%;
min-inline-size: 100%;
box-sizing: border-box;
margin: 0;
padding-block: var(--space-xs);
background: var(--surface1);
color: var(--text1);
font-family: system-ui, sans-serif;
}
h1,h2,h3 {
margin: 0;
font-weight: 500;
}
main {
display: grid;
gap: var(--space-xl);
place-content: center;
padding: var(--space-sm);
@media (width >= 540px) {
padding: var(--space-lg);
}
@media (width >= 800px) {
padding: var(--space-xl);
}
}
form {
max-width: 89vw;
display: grid;
gap: var(--space-xl) var(--space-xxl);
--repeat: auto-fit;
grid-template-columns:
repeat(var(--repeat), minmax(min(10ch, 100%), 35ch));
align-items: flex-start;
@media (orientation: landscape) and (width >= 640px) {
--repeat: 2;
}
}
section {
display: grid;
gap: var(--space-md);
}
header {
display: grid;
gap: var(--space-xxs);
}
fieldset {
border: 1px solid var(--surface4);
background: var(--surface4);
padding: 0;
margin: 0;
display: grid;
gap: 1px;
border-radius: var(--space-sm);
overflow: hidden;
transition: box-shadow .3s ease;
&:focus-within {
box-shadow: 0 5px 20px -10px hsl(0 0% 0% / 50%);
}
}
input[type="range"] {
--track-height: .5ex;
--track-fill: 0%;
--thumb-size: 3ex;
--thumb-offset: -1.25ex;
--thumb-highlight-size: 0px;
display: block;
inline-size: 100%;
margin: 1ex 0;
appearance: none;
background: transparent;
outline-offset: 5px;
@media (hover: none) {
--thumb-size: 30px;
--thumb-offset: -14px;
}
&::-webkit-slider-runnable-track {
appearance: none;
block-size: var(--track-height);
border-radius: 5ex;
background:
linear-gradient(
to right,
transparent var(--track-fill),
var(--surface1) 0%
),
var(--brand-bg-gradient) fixed;
}
&::-moz-range-track {
appearance: none;
block-size: var(--track-height);
border-radius: 5ex;
background:
linear-gradient(
to right,
transparent var(--track-fill),
var(--surface1) 0%
),
var(--brand-bg-gradient) fixed;
}
&::-webkit-slider-thumb {
appearance: none;
cursor: ew-resize;
border: 3px solid var(--surface3);
block-size: var(--thumb-size);
inline-size: var(--thumb-size);
margin-block-start: var(--thumb-offset);
border-radius: 50%;
background: var(--brand-bg-gradient) fixed;
box-shadow: 0 0 0 var(--thumb-highlight-size) var(--thumb-highlight-color);
@media (--motionOK) {
transition: box-shadow .1s ease;
}
@nest .fieldset-item:focus-within & {
border-color: var(--surface2);
}
}
&::-moz-range-thumb {
appearance: none;
cursor: ew-resize;
border: 3px solid var(--surface3);
block-size: var(--thumb-size);
inline-size: var(--thumb-size);
margin-block-start: var(--thumb-offset);
border-radius: 50%;
background: var(--brand-bg-gradient) fixed;
box-shadow: 0 0 0 var(--thumb-highlight-size) var(--thumb-highlight-color);
@media (--motionOK) {
transition: box-shadow .1s ease;
}
@nest .fieldset-item:focus-within & {
border-color: var(--surface2);
}
}
&:is(:hover,:active) {
--thumb-highlight-size: 10px;
}
}
input[type="checkbox"] {
inline-size: var(--space-sm);
block-size: var(--space-sm);
margin: 0;
outline-offset: 5px;
accent-color: var(--brand);
position: relative;
transform-style: preserve-3d;
cursor: pointer;
&:hover::before {
--thumb-scale: 1;
}
@media (hover: none) {
inline-size: var(--space-md);
block-size: var(--space-md);
}
&::before {
--thumb-scale: .01;
--thumb-highlight-size: var(--space-xl);
content: "";
inline-size: var(--thumb-highlight-size);
block-size: var(--thumb-highlight-size);
clip-path: circle(50%);
position: absolute;
inset-block-start: 50%;
inset-inline-start: 50%;
background: var(--thumb-highlight-color);
transform-origin: center center;
transform:
translateX(calc(var(--isRTL) * 50%))
translateY(-50%)
translateZ(-1px)
scale(var(--thumb-scale))
;
will-change: transform;
@media (--motionOK) {
transition: transform .2s ease;
}
}
}
.fieldset-item {
background: var(--surface3);
transition: background .2s ease;
display: grid;
grid-template-columns: var(--space-lg) 1fr;
gap: var(--space-md);
padding-block: var(--space-sm);
padding-inline: var(--space-md);
@media (width >= 540px) {
grid-template-columns: var(--space-xxl) 1fr;
gap: var(--space-xs);
padding-block: var(--space-md);
padding-inline: 0 var(--space-xl);
}
&:focus-within {
background: var(--surface2);
& svg {
fill: white;
}
& picture {
clip-path: circle(50%);
background: var(--brand-bg-gradient) fixed;
}
}
& > :is(.input-stack, label) {
display: grid;
gap: var(--space-xs);
}
& > .input-stack > label {
display: contents;
}
& > picture {
block-size: var(--space-xl);
inline-size: var(--space-xl);
clip-path: circle(40%);
display: inline-grid;
place-content: center;
background: var(--surface3) fixed;
@media (--motionOK) {
transition: clip-path .3s ease;
}
}
& svg {
fill: var(--text2);
block-size: var(--space-md);
}
& > :is(picture, input[type="checkbox"]) {
place-self: center;
}
}
small {
color: var(--text2);
line-height: 1.5;
}
This pattern shows how to build a settings component that is responsive, supports multiple device inputs, and works across browsers.
Full article · Video on YouTube · Source on Github
Sidenav
<aside id="sidenav-open">
<nav>
<h4>My</h4>
<a href="#">Dashboard</a>
<a href="#">Profile</a>
<a href="#">Preferences</a>
<a href="#">Archive</a>
<h4>Settings</h4>
<a href="#">Accessibility</a>
<a href="#">Theme</a>
<a href="#">Admin</a>
</nav>
<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu" onchange="history.go(-1)"></a>
</aside>
<main>
<header>
<a href="#sidenav-open" id="sidenav-button" class="hamburger" title="Open Menu" aria-label="Open Menu">
<svg viewBox="0 0 50 40" role="presentation" focusable="false" aria-label="trigram for heaven symbol">
<line x1="0" x2="100%" y1="10%" y2="10%" />
<line x1="0" x2="100%" y1="50%" y2="50%" />
<line x1="0" x2="100%" y1="90%" y2="90%" />
</svg>
</a>
<h1>Site Title</h1>
</header>
<article>
<h2>Totam Header</h2>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Cum consectetur, necessitatibus velit officia ut impedit veritatis temporibus soluta? Totam odit cupiditate facilis nisi sunt hic necessitatibus voluptatem nihil doloribus! Enim.</p>
<p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Fugit rerum, amet odio explicabo voluptas eos cum libero, ex esse quasi optio incidunt soluta eligendi labore error corrupti! Dolore, cupiditate porro.</p>
<h3>Subhead Totam Odit</h3>
<p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Fugit rerum, amet odio explicabo voluptas eos cum libero, ex esse quasi optio incidunt soluta eligendi labore error corrupti! Dolore, cupiditate porro.</p>
<p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Fugit rerum, amet odio explicabo voluptas eos cum libero, ex esse quasi optio incidunt soluta eligendi labore error corrupti! Dolore, cupiditate porro.</p>
<h3>Subhead</h3>
<p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Fugit rerum, amet odio explicabo voluptas eos cum libero, ex esse quasi optio incidunt soluta eligendi labore error corrupti! Dolore, cupiditate porro.</p>
<p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Fugit rerum, amet odio explicabo voluptas eos cum libero, ex esse quasi optio incidunt soluta eligendi labore error corrupti! Dolore, cupiditate porro.</p>
</article>
</main>
const sidenav = document.querySelector('#sidenav-open')
const closenav = document.querySelector('#sidenav-close')
const opennav = document.querySelector('#sidenav-button')
// set focus to our open/close buttons after animation
sidenav.addEventListener('transitionend', e => {
if (e.propertyName !== 'transform')
return
const isOpen = document.location.hash === '#sidenav-open'
isOpen
? closenav.focus()
: opennav.focus()
if (!isOpen) {
history.replaceState(history.state, '')
}
})
// close our menu when esc is pressed
sidenav.addEventListener('keyup', e => {
if (e.code === 'Escape')
window.history.length
? window.history.back()
: document.location.hash = ''
})
body {
display: grid;
grid: [stack] 1fr / min-content [stack] 1fr;
@media (max-width: 540px) {
& > :matches(aside, main) {
grid-area: stack;
}
}
}
#sidenav-open {
--easeOutExpo: cubic-bezier(0.16, 1, 0.3, 1);
--duration: .6s;
display: grid;
grid-template-columns: [nav] 2fr [escape] 1fr;
@media (max-width: 540px) {
position: sticky;
top: 0;
max-height: 100vh;
overflow: hidden auto;
overscroll-behavior: contain;
visibility: hidden; /* not keyboard accessible when closed */
transform: translateX(-110vw);
will-change: transform;
transition:
transform var(--duration) var(--easeOutExpo),
visibility 0s linear var(--duration);
&:target {
visibility: visible;
transform: translateX(0);
transition: transform var(--duration) var(--easeOutExpo);
}
}
@media (prefers-reduced-motion: reduce) {
--duration: 1ms;
}
}
#sidenav-button,
#sidenav-close {
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
user-select: none;
touch-action: manipulation;
@media (min-width: 540px) {
display: none;
}
}
This pattern shows how to build a component for the web that is responsive, stateful, supports keyboard navigation, works with and without JavaScript, and works across browsers.
Full article · Video on YouTube · Source on Github
Split Buttons
<div class="gui-split-button">
<button>View Cart</button>
<span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
<svg aria-hidden="true" viewBox="0 0 20 20">
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
</svg>
<ul class="gui-popup">
<li><button>
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
Checkout
</button></li>
<li><button>
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
</svg>
Quick Pay
</button></li>
<li><button>
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
</svg>
Save for later
</button></li>
</ul>
</span>
</div>
<div class="gui-split-button">
<button>Send</button>
<span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
<svg aria-hidden="true" viewBox="0 0 20 20">
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
</svg>
<ul class="gui-popup">
<li><button>
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Schedule for later
</button></li>
<li><button>
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Delete
</button></li>
<li><button>
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
</svg>
Save draft
</button></li>
</ul>
</span>
</div>
<div class="gui-split-button">
<button>Squash</button>
<span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
<svg aria-hidden="true" viewBox="0 0 20 20">
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
</svg>
<ul class="gui-popup">
<li><button>
Create a merge commit
</button></li>
<li><button>
Rebase
</button></li>
</ul>
</span>
</div>
import $ from 'blingblingjs'
import {rovingIndex} from 'roving-ux'
const splitButtons = $('.gui-split-button')
const popupButtons = $('.gui-popup-button')
// popup activating roving index for it's buttons
popupButtons.forEach(element =>
rovingIndex({
element,
target: 'button',
}))
// support escape key
popupButtons.on('keyup', e => {
if (e.code === 'Escape')
e.target.blur()
})
popupButtons.on('focusin', e => {
e.currentTarget.setAttribute('aria-expanded', true)
})
popupButtons.on('focusout', e => {
e.currentTarget.setAttribute('aria-expanded', false)
})
// respond to any button interaction
splitButtons.on('click', event => {
if (event.target.nodeName !== 'BUTTON') return
console.info(event.target.innerText)
})
.gui-split-button {
--theme: hsl(220 75% 50%);
--theme-hover: hsl(220 75% 45%);
--theme-active: hsl(220 75% 40%);
--theme-text: hsl(220 75% 25%);
--theme-border: hsl(220 50% 75%);
--ontheme: hsl(220 90% 98%);
--popupbg: hsl(220 0% 100%);
--border: 1px solid var(--theme-border);
--radius: 6px;
--in-speed: 500ms;
--out-speed: 100ms;
display: inline-flex;
border-radius: var(--radius);
background: var(--theme);
color: var(--ontheme);
fill: var(--ontheme);
touch-action: manipulation;
user-select: none;
-webkit-tap-highlight-color: transparent;
@media (--dark) {
--theme: hsl(220 50% 60%);
--theme-hover: hsl(220 50% 65%);
--theme-active: hsl(220 75% 70%);
--theme-text: hsl(220 10% 85%);
--theme-border: hsl(220 20% 70%);
--ontheme: hsl(220 90% 5%);
--popupbg: hsl(220 10% 30%);
}
& button {
cursor: pointer;
appearance: none;
background: none;
border: none;
display: inline-flex;
align-items: center;
gap: 1ch;
white-space: nowrap;
font-family: inherit;
font-size: inherit;
font-weight: 500;
padding-block: 1.25ch;
padding-inline: 2.5ch;
color: var(--ontheme);
outline-color: var(--theme);
outline-offset: -5px;
&:is(:hover, :focus-visible) {
background: var(--theme-hover);
color: var(--ontheme);
& > svg {
stroke: currentColor;
fill: none;
}
}
&:active {
background: var(--theme-active);
}
}
& > button {
border-radius: var(--radius) 0 0 var(--radius);
@supports (border-start-start-radius: 1px) {
border-end-start-radius: var(--radius);
border-start-start-radius: var(--radius);
}
}
@media (--light) {
& > button,
& button:is(:focus-visible, :hover) {
text-shadow: 0 1px 0 var(--theme-active);
}
& > .gui-popup-button > svg,
& button:is(:focus-visible, :hover) > svg {
filter: drop-shadow(0 1px 0 var(--theme-active));
}
}
& svg {
inline-size: 2ch;
box-sizing: content-box;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2px;
}
}
.gui-popup-button {
inline-size: 4ch;
cursor: pointer;
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
border-inline-start: var(--border);
border-radius: 0 var(--radius) var(--radius) 0;
@supports (border-start-start-radius: 1px) {
border-inline-start: var(--border);
border-start-end-radius: var(--radius);
border-end-end-radius: var(--radius);
}
&:is(:hover,:focus-within) {
background: var(--theme-hover);
}
/* fixes iOS trying to be helpful */
&:focus {
outline: none;
}
&:active {
background: var(--theme-active);
}
&:focus-within {
& > svg {
transition-duration: var(--in-speed);
transform: rotateZ(.5turn);
}
& > .gui-popup {
transition-duration: var(--in-speed);
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
}
@media (--motionOK) {
& > svg {
transition: transform var(--out-speed) ease;
}
& > .gui-popup {
transform: translateY(5px);
transition:
opacity var(--out-speed) ease,
transform var(--out-speed) ease;
}
}
}
.gui-popup {
--shadow: 220 70% 15%;
--shadow-strength: 1%;
opacity: 0;
pointer-events: none;
position: absolute;
inset-block-end: 80%;
inset-inline-start: -1.5ch;
list-style-type: none;
background: var(--popupbg);
color: var(--theme-text);
padding-inline: 0;
padding-block: .5ch;
border-radius: var(--radius);
overflow: hidden;
display: flex;
flex-direction: column;
font-size: .9em;
transition: opacity var(--out-speed) ease;
box-shadow:
0 -2px 5px 0 hsl(var(--shadow) / calc(var(--shadow-strength) + 5%)),
0 1px 1px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 10%)),
0 2px 2px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 12%)),
0 5px 5px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 13%)),
0 9px 9px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 14%)),
0 16px 16px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 20%))
;
/* fixes iOS trying to be helpful */
&:focus {outline: none}
@media (--dark) {
--shadow-strength: 5%;
--shadow: 220 3% 2%;
& button:not(:focus-visible, :hover) {
text-shadow: 0 1px 0 var(--ontheme);
}
& button:not(:focus-visible, :hover) > svg {
filter: drop-shadow(0 1px 0 var(--ontheme));
}
}
@media (width <= 400px) {
inset-inline-start: -200%;
}
& svg {
fill: var(--popupbg);
stroke: var(--theme);
@media (prefers-color-scheme: dark) {
stroke: var(--theme-border);
}
}
& button {
color: var(--theme-text);
width: 100%;
}
}
This pattern shows how to build an adaptive and accessible split button component.
Note: this component uses a library called BlingBlingJS and Roving UX.
Full article · Video on YouTube · Source on Github
Stories
<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>
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
const state = {
current_story: stories.firstElementChild.lastElementChild
}
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
}
}
}
stories.addEventListener('click', e => {
if (e.target.nodeName !== 'ARTICLE')
return
navigateStories(
e.clientX > median
? 'next'
: 'prev')
})
// left & right are free with snap points 👍
document.addEventListener('keydown', ({key}) => {
if (key !== 'ArrowDown' || key !== 'ArrowUp')
navigateStories(
key === 'ArrowDown'
? 'next'
: 'prev')
})
.stories {
display: grid;
grid: 1fr / auto-flow 100%;
grid-gap: 1ch;
gap: 1ch;
overflow-x: auto;
scroll-snap-type: x mandatory;
overscroll-behavior: contain;
touch-action: pan-x;
}
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
display: grid;
grid: [story] 1fr / [story] 1fr;
}
.story {
grid-area: story;
background-size: cover;
background-image:
var(--bg),
linear-gradient(to top, rgb(249, 249, 249), rgb(226, 226, 226));
user-select: none;
touch-action: manipulation;
transition: opacity .3s cubic-bezier(0.4, 0.0, 1, 1)
}
.story.seen {
opacity: 0;
pointer-events: none;
}
This pattern shows how to build a Stories component for the web that is responsive, supports keyboard navigation, and works across browsers.
Full article · Video on YouTube · Source on Github
SVG Favicon
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 447 428" width="400" height="400">
<style>
.favicon-stroke {
stroke-width: 8px;
stroke: #8929ff;
}
#skull-outline { fill: white }
#eyes-and-nose, #hat-outline { fill: #8929ff }
#hat-fill, #hat-bill { fill: #e662e6 }
@media (prefers-color-scheme: dark) {
.favicon-stroke { stroke: #343a40 }
#skull-outline { fill: #adb5bd }
#hat-outline { fill: #343a40 }
#eyes-and-nose { fill: #343a40 }
}
</style>
<g id="skull">
<path id="skull-outline" class="favicon-stroke" stroke-linejoin="round" d="M19.62 188.39A166.62 166.62 0 0 1 186.24 21.77c115.25 0 166.61 74.59 166.61 166.62 0 1.83-.08 3.64-.13 5.46h.13s.68 175.09.68 178.65c0 30.11-16.26 41.67-36.32 41.67-12.7 0-35.22-3.93-36.22-32.69h-.2c-1 28.76-16.81 32.69-36.22 32.69-18 0-32.87-6.78-35.77-32.53-2.9 25.75-17.8 32.53-35.8 32.53-20.06 0-36.32-11.56-36.32-41.67 0-2.48.36-24.88.36-24.88A166.68 166.68 0 0 1 19.62 188.39Z" />
<path id="eyes-and-nose" d="M180.77 205.76c0 23.64 12.84 42.81 28.68 42.81s28.68-19.17 28.68-42.81-12.84-42.82-28.68-42.82-28.68 19.17-28.68 42.82M275 205.76c0 23.64 12.84 42.81 28.68 42.81s28.68-19.17 28.68-42.81-12.84-42.82-28.68-42.82S275 182.11 275 205.76M264.51 276.85s-29.26 43.53-20.12 49.23c7.07 4.41 20.49-16.71 20.49-16.71s12.82 22.58 16.76 20c16.24-10.71-17.13-52.5-17.13-52.5"/>
<path id="jawline" class="favicon-stroke" fill="none" stroke-linecap="round" d="M114.92 284.33c22.54-1 22 7 22 62.48" />
</g>
<g id="hat">
<path id="hat-fill" d="m36.27 118.44 2-5.41c.37-1 9.25-24.14 33-47.53 21.83-21.56 60.88-47.26 122.91-47.26C258 18.24 294.8 44 314.46 65.65c21.38 23.52 27.49 46.82 27.74 47.8l1.27 5Z"/>
<path id="hat-outline" d="M194.17 22.24c120.68 0 144.15 92.2 144.15 92.2H42.05s34.67-92.2 152.12-92.2m0-8c-27.9 0-53.65 5.07-76.52 15.08a162.3 162.3 0 0 0-49.21 33.33c-24.31 24-33.5 48-33.88 49l-4.07 10.82h318.13l-2.54-10c-.26-1-6.62-25.26-28.66-49.51a140.29 140.29 0 0 0-46.52-33.6c-22.25-10-48.06-15.12-76.73-15.12"/>
<path id="hat-bill" class="favicon-stroke" stroke-linecap="square" d="M350.7 117.76c69.17 0 112.9-106.93 57.6-106.93-81 0-95.18 74.6-198.47 106.93M80.32 117.86h267.53"/>
</g>
</svg>
This pattern shows how to build an adaptive favicon with SVG.
Full article · Video on YouTube · Source on Github
Switch
<label for="switch-1" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-1">
</label>
<label for="switch-2" class="gui-switch">
Indeterminate
<input type="checkbox" role="switch" id="switch-2">
<script>document.getElementById('switch-2').indeterminate = true</script>
</label>
<label for="switch-3" class="gui-switch">
Disabled
<input type="checkbox" role="switch" id="switch-3" disabled>
</label>
<label for="switch-4" class="gui-switch">
Disabled (checked)
<input type="checkbox" role="switch" id="switch-4" disabled checked>
</label>
<label for="switch-vertical" class="gui-switch -vertical">
Vertical
<input type="checkbox" role="switch" id="switch-vertical">
</label>
const elements = document.querySelectorAll('.gui-switch')
const switches = new WeakMap()
const state = {
activethumb: null,
recentlyDragged: false,
}
const getStyle = (element, prop) =>
parseInt(
window.getComputedStyle(element)
.getPropertyValue(prop))
const getPseudoStyle = (element, prop) =>
parseInt(
window.getComputedStyle(element, ':before')
.getPropertyValue(prop))
const dragInit = event => {
if (event.target.disabled) return
state.activethumb = event.target
state.activethumb.addEventListener('pointermove', dragging)
state.activethumb.style.setProperty('--thumb-transition-duration', '0s')
}
const dragging = event => {
if (!state.activethumb) return
let {thumbsize, bounds, padding} = switches.get(state.activethumb.parentElement)
let directionality = getStyle(state.activethumb, '--isLTR')
let track = (directionality === -1)
? (state.activethumb.clientWidth * -1) + thumbsize + padding
: 0
let pos = Math.round(event.offsetX - thumbsize / 2)
if (pos < bounds.lower) pos = 0
if (pos > bounds.upper) pos = bounds.upper
state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
}
const dragEnd = event => {
if (!state.activethumb) return
state.activethumb.checked = determineChecked()
if (state.activethumb.indeterminate)
state.activethumb.indeterminate = false
state.activethumb.style.removeProperty('--thumb-transition-duration')
state.activethumb.style.removeProperty('--thumb-position')
state.activethumb.removeEventListener('pointermove', dragging)
state.activethumb = null
padRelease()
}
const padRelease = () => {
state.recentlyDragged = true
setTimeout(_ => {
state.recentlyDragged = false
}, 300)
}
const preventBubbles = event => {
if (state.recentlyDragged)
event.preventDefault() && event.stopPropagation()
}
const labelClick = event => {
if (
state.recentlyDragged ||
!event.target.classList.contains('gui-switch') ||
event.target.querySelector('input').disabled
) return
let checkbox = event.target.querySelector('input')
checkbox.checked = !checkbox.checked
event.preventDefault()
}
const determineChecked = () => {
let {bounds} = switches.get(state.activethumb.parentElement)
let curpos =
Math.abs(
parseInt(
state.activethumb.style.getPropertyValue('--thumb-position')))
if (!curpos) {
curpos = state.activethumb.checked
? bounds.lower
: bounds.upper
}
return curpos >= bounds.middle
}
elements.forEach(guiswitch => {
let checkbox = guiswitch.querySelector('input')
let thumbsize = getPseudoStyle(checkbox, 'width')
let padding = getStyle(checkbox, 'padding-left') + getStyle(checkbox, 'padding-right')
checkbox.addEventListener('pointerdown', dragInit)
checkbox.addEventListener('pointerup', dragEnd)
checkbox.addEventListener('click', preventBubbles)
guiswitch.addEventListener('click', labelClick)
switches.set(guiswitch, {
thumbsize,
padding,
bounds: {
lower: 0,
middle: (checkbox.clientWidth - padding) / 4,
upper: checkbox.clientWidth - thumbsize - padding,
},
})
})
window.addEventListener('pointerup', event => {
if (!state.activethumb) return
dragEnd(event)
})
.gui-switch {
--thumb-size: 2rem;
--thumb: hsl(0 0% 100%);
--thumb-highlight: hsl(0 0% 0% / 25%);
--track-size: calc(var(--thumb-size) * 2);
--track-padding: 2px;
--track-inactive: hsl(80 0% 80%);
--track-active: hsl(80 60% 45%);
--thumb-color: var(--thumb);
--thumb-color-highlight: var(--thumb-highlight);
--track-color-inactive: var(--track-inactive);
--track-color-active: var(--track-active);
--isLTR: 1;
display: flex;
align-items: center;
gap: 2ch;
justify-content: space-between;
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
@media (prefers-color-scheme: dark) {
--thumb: hsl(0 0% 5%);
--thumb-highlight: hsl(0 0% 100% / 25%);
--track-inactive: hsl(80 0% 35%);
--track-active: hsl(80 60% 60%);
}
&:dir(rtl) {
--isLTR: -1;
}
&.-vertical {
min-block-size: calc(var(--track-size) + calc(var(--track-padding) * 2));
& > input {
transform: rotate(calc(90deg * var(--isLTR) * -1));
touch-action: pan-x;
}
}
& > input {
--thumb-position: 0%;
--thumb-transition-duration: .25s;
padding: var(--track-padding);
background: var(--track-color-inactive);
inline-size: var(--track-size);
block-size: var(--thumb-size);
border-radius: var(--track-size);
appearance: none;
pointer-events: none;
touch-action: pan-y;
border: none;
outline-offset: 5px;
box-sizing: content-box;
flex-shrink: 0;
display: grid;
align-items: center;
grid: [track] 1fr / [track] 1fr;
transition: background-color .25s ease;
&::before {
--highlight-size: 0;
content: "";
cursor: pointer;
pointer-events: auto;
grid-area: track;
inline-size: var(--thumb-size);
block-size: var(--thumb-size);
background: var(--thumb-color);
box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);
border-radius: 50%;
transform: translateX(var(--thumb-position));
@media (--motionOK) { & {
transition:
transform var(--thumb-transition-duration) ease,
box-shadow .25s ease;
}}
}
&:not(:disabled):hover::before {
--highlight-size: .5rem;
}
&:checked {
background: var(--track-color-active);
--thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}
&:indeterminate {
--thumb-position: calc(
calc(calc(var(--track-size) / 2) - calc(var(--thumb-size) / 2))
* var(--isLTR)
);
}
&:disabled {
cursor: not-allowed;
--thumb-color: transparent;
&::before {
cursor: not-allowed;
box-shadow: inset 0 0 0 2px hsl(0 0% 100% / 50%);
@media (prefers-color-scheme: dark) {
box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 50%);
}
}
}
}
}
This pattern shows how to create a responsive, adaptive and accessible switch component.
Full article · Video on YouTube · Source on Github
Tabs
<snap-tabs>
<header class="scroll-snap-x">
<nav>
<a active href="#responsive">Responsive</a>
<a href="#accessible">Accessible</a>
<a href="#overscroll">Horizontal Overscroll Ready</a>
<a href="#more"><!-- ...SVG icon --></a>
</nav>
<span class="snap-indicator"></span>
</header>
<section class="scroll-snap-x">
<article id="responsive">
<!-- ...content -->
</article>
<article id="accessible">
<!-- ...content -->
</article>
<article id="overscroll">
<!-- ...content -->
</article>
<article id="more">
<!-- ...content -->
</article>
</section>
</snap-tabs>
import 'https://argyleink.github.io/scroll-timeline/dist/scroll-timeline.js'
const {matches:motionOK} = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
// grab and stash elements
const tabgroup = document.querySelector('snap-tabs')
const tabsection = tabgroup.querySelector(':scope > section')
const tabnav = tabgroup.querySelector(':scope nav')
const tabnavitems = tabnav.querySelectorAll(':scope a')
const tabindicator = tabgroup.querySelector(':scope .snap-indicator')
/*
shared timeline for .indicator
and nav > a colors */
const sectionScrollTimeline = new ScrollTimeline({
scrollSource: tabsection,
orientation: 'inline',
fill: 'both',
})
/*
for each nav link
- animate color based on the scroll timeline
- color is active when its the current index*/
tabnavitems.forEach(navitem => {
navitem.animate({
color: [...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
)
})
if (motionOK) {
tabindicator.animate({
transform: [...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`),
width: [...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
)
}
const setActiveTab = tabbtn => {
tabnav
.querySelector(':scope a[active]')
.removeAttribute('active')
tabbtn.setAttribute('active', '')
tabbtn.scrollIntoView()
}
const determineActiveTabSection = () => {
const i = tabsection.scrollLeft / tabsection.clientWidth
const matchingNavItem = tabnavitems[i]
matchingNavItem && setActiveTab(matchingNavItem)
}
tabnav.addEventListener('click', e => {
if (e.target.nodeName !== "A") return
setActiveTab(e.target)
})
tabsection.addEventListener('scroll', () => {
clearTimeout(tabsection.scrollEndTimer)
tabsection.scrollEndTimer = setTimeout(
determineActiveTabSection
, 100)
})
window.onload = () => {
if (location.hash)
tabsection.scrollLeft = document
.querySelector(location.hash)
.offsetLeft
determineActiveTabSection()
}
snap-tabs {
--hue: 328deg;
--accent: var(--hue) 100% 54%;
--indicator-size: 2px;
--space-1: .5rem;
--space-2: 1rem;
--space-3: 1.5rem;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
& :matches(header, nav, section, article, a) {
outline-color: hsl(var(--accent));
outline-offset: -5px;
}
}
.scroll-snap-x {
overflow: auto hidden;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
@media (hover: none) {
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
}
snap-tabs > header {
--text-color: hsl(var(--hue) 5% 40%);
--text-active-color: hsl(var(--hue) 20% 10%);
flex-shrink: 0;
min-block-size: fit-content;
display: flex;
flex-direction: column;
& > nav {
display: flex;
}
& a {
scroll-snap-align: start;
display: inline-flex;
align-items: center;
white-space: nowrap;
font-size: .8rem;
color: var(--text-color);
font-weight: bold;
text-decoration: none;
padding: var(--space-2) var(--space-3);
& > svg {
inline-size: 1.5em;
pointer-events: none;
}
&:hover {
background: hsl(var(--accent) / 5%);
}
&:focus {
outline-offset: -.5ch;
}
}
& > .snap-indicator {
inline-size: 0;
block-size: var(--indicator-size);
border-radius: var(--indicator-size);
background: hsl(var(--accent));
}
}
snap-tabs > section {
block-size: 100%;
display: grid;
grid-auto-flow: column;
grid-auto-columns: 100%;
& > article {
scroll-snap-align: start;
overflow-y: auto;
overscroll-behavior-y: contain;
padding: var(--space-2) var(--space-3);
}
}
@media (prefers-reduced-motion: reduce) {
/*
- swap to border-bottom styles
- transition colors
- hide the animated .indicator
*/
snap-tabs {
& > header a {
border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
transition:
color .7s ease,
border-color .5s ease;
&:matches(:target,:active,[active]) {
color: var(--text-active-color);
border-block-end-color: hsl(var(--accent));
}
}
& .snap-indicator {
visibility: hidden;
}
}
}
This pattern shows how to create a tab component with grid and scroll snap points.
Note: this component uses a polyfill for Scroll Timeline to create the scroll linked animation underline.
Full article · Video on YouTube · Source on Github
Toast
<button id="spells">
Cast Spell
</button>
<button id="actions">
Mock User Action
</button>
const init = () => {
const node = document.createElement('section')
node.classList.add('gui-toast-group')
document.firstElementChild.insertBefore(node, document.body)
return node
}
const createToast = text => {
const node = document.createElement('output')
node.innerText = text
node.classList.add('gui-toast')
node.setAttribute('role', 'status')
node.setAttribute('aria-live', 'polite')
return node
}
const addToast = toast => {
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
Toaster.children.length && motionOK
? flipToast(toast)
: Toaster.appendChild(toast)
}
const Toast = text => {
let toast = createToast(text)
addToast(toast)
return new Promise(async (resolve, reject) => {
await Promise.allSettled(
toast.getAnimations().map(animation =>
animation.finished
)
)
Toaster.removeChild(toast)
resolve()
})
}
// https://aerotwist.com/blog/flip-your-animations/
const flipToast = toast => {
// FIRST
const first = Toaster.offsetHeight
// add new child to change container size
Toaster.appendChild(toast)
// LAST
const last = Toaster.offsetHeight
// INVERT
const invert = last - first
// PLAY
const animation = Toaster.animate([
{ transform: `translateY(${invert}px)` },
{ transform: 'translateY(0)' }
], {
duration: 150,
easing: 'ease-out',
})
animation.startTime = document.timeline.currentTime
}
const Toaster = init()
export default Toast
.gui-toast-group {
position: fixed;
z-index: 1;
inset-block-end: 0;
inset-inline: 0;
padding-block-end: 5vh;
display: grid;
justify-items: center;
justify-content: center;
gap: 1vh;
/* optimizations */
pointer-events: none;
}
.gui-toast {
--_duration: 3s;
--_bg-lightness: 90%;
--_travel-distance: 0;
font-family: system-ui, sans-serif;
color: black;
background: hsl(0 0% var(--_bg-lightness) / 90%);
max-inline-size: min(25ch, 90vw);
padding-block: .5ch;
padding-inline: 1ch;
border-radius: 3px;
font-size: 1rem;
will-change: transform;
animation:
fade-in .3s ease,
slide-in .3s ease,
fade-out .3s ease var(--_duration);
@media (--dark) {
color: white;
--_bg-lightness: 20%;
}
@media (--motionOK) {
--_travel-distance: 5vh;
}
}
@keyframes fade-in {
from { opacity: 0 }
}
@keyframes fade-out {
to { opacity: 0 }
}
@keyframes slide-in {
from { transform: translateY(var(--_travel-distance, 10px)) }
}
This pattern shows how to build an adaptive and accessible toast component.
Full article · Video on YouTube · Source on Github