Animacje, motywy, komponenty i inne wzorce układu są już dostępne, aby ułatwić rozpoczęcie pracy z Twoim interfejsem i jego UX.
Z przyjemnością udostępniam wiele nowych wzorców web.dev. Te nowości pochodzą z programu GUI Challenges, w którym przedstawiam swoje strategie tworzenia różnych komponentów i wspólnych potrzeb interfejsu, a następnie zbieram zgłoszenia użytkowników do tych samych zadań i pomagamy nam wszystkim lepiej rozumieć, jak sprostać tym wymaganiom.
Okazuje się, że Wyzwania GUI idealnie pasują do wzorców:
<h1 split-by="word" word-animation="hover">
hover the words
</h1>
@media (prefers-reduced-motion:no-preference) {
[word-animation] {
display: inline-flex;
flex-wrap: wrap;
gap: 1ch
}
}
@media (prefers-reduced-motion:no-preference) and (hover) {
[word-animation=hover] {
overflow: hidden;
overflow: clip
}
[word-animation=hover]>span {
transition: transform .3s ease;
cursor: pointer
}
[word-animation=hover]>span:not(:hover) {
transform: translateY(50%)
}
}
const span = (text, index) => {
const node = document.createElement('span')
node.textContent = text
node.style.setProperty('--index', index)
return node
}
export const byWord = text =>
text.split(' ').map(span)
const {matches:motionOK} = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
if (motionOK) {
const splitTargets = document.querySelectorAll('[split-by]')
splitTargets.forEach(node => {
let nodes = byWord(node.innerText)
if (nodes)
node.firstChild.replaceWith(...nodes)
})
}
Teraz można je umieszczać w postach (jak powyżej), łączyć je, aby ułatwić ich przeglądanie i inspirować, oraz dodawać nowe kategorie, do których inni użytkownicy mogą dodawać swoje wzorce. Rozejrzyj się dookoła, pobierz jakiś kod – znajdziesz tam wszystkie potrzebne informacje.
Przegląd
Trzy nowe kategorie wzorów:
Dodatkowo do istniejących wzorców Układ dodano 5 nowych wzorców.
Komponenty
Wyświetl stronę docelową wzorców komponentów lub sprawdź każdy element osobno:
- Menu nawigacyjne
- przyciski;
- Karuzela
- Okno
- Menu gry
- Pasek wczytywania
- Przewijanie multimediów
- Wybór wielokrotny
- Settings
- Pasek boczny
- Przyciski podzielone
- Relacje
- Favikona SVG
- Przełącz
- Karty
- Toast
Oto podgląd wzorca przycisku podziału:
<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>
.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%;
}
}
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)
})
Animacje
Wyświetl stronę docelową wzorców animacji lub sprawdź każdy z nich po kolei:
Oto podgląd animowanego wzorca liter:
<h1 split-by="letter" letter-animation="breath">
animated letters
</h1>
@keyframes breath {
from {
animation-timing-function: ease-out;
}
to {
transform: scale(1.25) translateY(-5px) perspective(1px);
text-shadow: 0 0 40px var(--glow-color);
animation-timing-function: ease-in-out;
}
}
@media (prefers-reduced-motion:no-preference) {
[letter-animation] > span {
display: inline-block;
white-space: break-spaces;
}
[letter-animation=breath] {
--glow-color: white;
}
[letter-animation=breath]>span {
animation: breath 1.2s ease calc(var(--index) * 100 * 1ms) infinite alternate;
}
}
@media (prefers-reduced-motion:no-preference) and (prefers-color-scheme: light) {
[letter-animation=breath] {
--glow-color: black;
}
}
const span = (text, index) => {
const node = document.createElement('span')
node.textContent = text
node.style.setProperty('--index', index)
return node
}
const byLetter = text =>
[...text].map(span)
const {matches:motionOK} = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
if (motionOK) {
const splitTargets = document.querySelectorAll('[split-by]')
splitTargets.forEach(node => {
let nodes = byLetter(node.innerText)
if (nodes)
node.firstChild.replaceWith(...nodes)
})
}
Motywy
Wyświetl stronę docelową wzorców motywów lub sprawdź każdy z nich po kolei:
Jeden wzorzec służy do tworzenia przełącznika motywu po stronie klienta, który pozwala użytkownikom wskazać swoje preferencje bez bezpośredniego powiązania z preferencjami systemowymi. Drugi służy do utworzenia systemu projektowania motywów z niestandardowymi właściwościami CSS.
Oto podgląd wzoru schematu kolorów:
<header>
<h3>Scheme</h3>
<form id="theme-switcher">
<div>
<input checked type="radio" id="auto" name="theme" value="auto">
<label for="auto">Auto</label>
</div>
<div>
<input type="radio" id="light" name="theme" value="light">
<label for="light">Light</label>
</div>
<div>
<input type="radio" id="dark" name="theme" value="dark">
<label for="dark">Dark</label>
</div>
<div>
<input type="radio" id="dim" name="theme" value="dim">
<label for="dim">Dim</label>
</div>
</form>
</header>
<main>
<section>
<div class="surface-samples">
<div class="surface1 rad-shadow">1</div>
<div class="surface2 rad-shadow">2</div>
<div class="surface3 rad-shadow">3</div>
<div class="surface4 rad-shadow">4</div>
</div>
</section>
<section>
<div class="text-samples">
<h1 class="text1">
<span class="swatch brand rad-shadow"></span>
Brand
</h1>
<h1 class="text1">
<span class="swatch text1 rad-shadow"></span>
Text Color 1
</h1>
<h1 class="text2">
<span class="swatch text2 rad-shadow"></span>
Text Color 2
</h1>
<br>
<p class="text1">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<p class="text2">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
</div>
</section>
</main>
* {
/* brand foundation */
--brand-hue: 200;
--brand-saturation: 100%;
--brand-lightness: 50%;
/* light */
--brand-light: hsl(var(--brand-hue) var(--brand-saturation) var(--brand-lightness));
--text1-light: hsl(var(--brand-hue) var(--brand-saturation) 10%);
--text2-light: hsl(var(--brand-hue) 30% 30%);
--surface1-light: hsl(var(--brand-hue) 25% 90%);
--surface2-light: hsl(var(--brand-hue) 20% 99%);
--surface3-light: hsl(var(--brand-hue) 20% 92%);
--surface4-light: hsl(var(--brand-hue) 20% 85%);
--surface-shadow-light: var(--brand-hue) 10% 20%;
--shadow-strength-light: .02;
/* dark */
--brand-dark: hsl(
var(--brand-hue)
calc(var(--brand-saturation) / 2)
calc(var(--brand-lightness) / 1.5)
);
--text1-dark: hsl(var(--brand-hue) 15% 85%);
--text2-dark: hsl(var(--brand-hue) 5% 65%);
--surface1-dark: hsl(var(--brand-hue) 10% 10%);
--surface2-dark: hsl(var(--brand-hue) 10% 15%);
--surface3-dark: hsl(var(--brand-hue) 5% 20%);
--surface4-dark: hsl(var(--brand-hue) 5% 25%);
--surface-shadow-dark: var(--brand-hue) 50% 3%;
--shadow-strength-dark: .8;
/* dim */
--brand-dim: hsl(
var(--brand-hue)
calc(var(--brand-saturation) / 1.25)
calc(var(--brand-lightness) / 1.25)
);
--text1-dim: hsl(var(--brand-hue) 15% 75%);
--text2-dim: hsl(var(--brand-hue) 10% 61%);
--surface1-dim: hsl(var(--brand-hue) 10% 20%);
--surface2-dim: hsl(var(--brand-hue) 10% 25%);
--surface3-dim: hsl(var(--brand-hue) 5% 30%);
--surface4-dim: hsl(var(--brand-hue) 5% 35%);
--surface-shadow-dim: var(--brand-hue) 30% 13%;
--shadow-strength-dim: .2;
}
:root {
color-scheme: light;
/* set defaults */
--brand: var(--brand-light);
--text1: var(--text1-light);
--text2: var(--text2-light);
--surface1: var(--surface1-light);
--surface2: var(--surface2-light);
--surface3: var(--surface3-light);
--surface4: var(--surface4-light);
--surface-shadow: var(--surface-shadow-light);
--shadow-strength: var(--shadow-strength-light);
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--brand: var(--brand-dark);
--text1: var(--text1-dark);
--text2: var(--text2-dark);
--surface1: var(--surface1-dark);
--surface2: var(--surface2-dark);
--surface3: var(--surface3-dark);
--surface4: var(--surface4-dark);
--surface-shadow: var(--surface-shadow-dark);
--shadow-strength: var(--shadow-strength-dark);
}
}
[color-scheme="light"] {
color-scheme: light;
--brand: var(--brand-light);
--text1: var(--text1-light);
--text2: var(--text2-light);
--surface1: var(--surface1-light);
--surface2: var(--surface2-light);
--surface3: var(--surface3-light);
--surface4: var(--surface4-light);
--surface-shadow: var(--surface-shadow-light);
--shadow-strength: var(--shadow-strength-light);
}
[color-scheme="dark"] {
color-scheme: dark;
--brand: var(--brand-dark);
--text1: var(--text1-dark);
--text2: var(--text2-dark);
--surface1: var(--surface1-dark);
--surface2: var(--surface2-dark);
--surface3: var(--surface3-dark);
--surface4: var(--surface4-dark);
--surface-shadow: var(--surface-shadow-dark);
--shadow-strength: var(--shadow-strength-dark);
}
[color-scheme="dim"] {
color-scheme: dark;
--brand: var(--brand-dim);
--text1: var(--text1-dim);
--text2: var(--text2-dim);
--surface1: var(--surface1-dim);
--surface2: var(--surface2-dim);
--surface3: var(--surface3-dim);
--surface4: var(--surface4-dim);
--surface-shadow: var(--surface-shadow-dim);
--shadow-strength: var(--shadow-strength-dim);
}
/* READY TO USE! */
.brand {
color: var(--brand);
background-color: var(--brand);
}
.surface1 {
background-color: var(--surface1);
color: var(--text2);
}
.surface2 {
background-color: var(--surface2);
color: var(--text2);
}
.surface3 {
background-color: var(--surface3);
color: var(--text1);
}
.surface4 {
background-color: var(--surface4);
color: var(--text1);
}
.text1 {
color: var(--text1);
}
p.text1 {
font-weight: 200;
}
.text2 {
color: var(--text2);
}
const switcher = document.querySelector('#theme-switcher')
const doc = document.firstElementChild
switcher.addEventListener('input', e =>
setTheme(e.target.value))
const setTheme = theme =>
doc.setAttribute('color-scheme', theme)
Nowe wzorce układu po wyśrodkowaniu
Wyświetl stronę docelową wzorców układu lub sprawdź poszczególne elementy:
W każdej wersji demonstracyjnej znajduje się uchwyt do zmiany rozmiaru kontenera oraz przycisk umożliwiający dodanie do układu elementu podrzędnego. Dzięki nim, jak objaśniono w tym artykule, możesz poznać mocne i słabe strony różnych metod centrowania dostępnych w internecie. Poza tym mają zabawne nazwy.
Oto artykuł wyłoniony „zwycięzcą” eksploracji środkowej, czyli „Delikatny Flex”:
<article class="gentle-flex">
<h1>Gentle Flex</h1>
</article>
.gentle-flex {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1ch;
}
Podsumowanie
Mam nadzieję, że te nowe wzorce pomogą Ci nauczyć się nowych technik, zainspirować Cię, przekazać spostrzeżenia na temat ułatwień dostępu i ogólnie utrzymać zainteresowanie tworzeniem UI. Zespół Chrome będzie stale dodawać nowe wzorce do kolekcji.
- Publikuj banery powitalne wygenerowane przez fantastycznego penta pomocy George'a Francisa.