Pola baru

Animasi, tema, komponen, dan pola tata letak lainnya sudah aktif dan siap membantu memulai atau menginspirasi UI dan UX Anda.

Saya senang membagikan banyak pola web.dev baru. Penambahan ini berasal dari acara GUI Challenges yang membagikan strategi saya tentang cara mem-build berbagai komponen dan kebutuhan antarmuka yang umum, lalu mengumpulkan kiriman pengguna untuk tugas yang sama dan membantu kita semua mengembangkan perspektif tentang cara menyelesaikannya.

Ternyata Tantangan GUI sangat cocok dengan pola:

HTML

<h1 split-by="word" word-animation="hover">
  hover the words
</h1>

CSS


        @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%)
  }
}
        

JS


        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)
  })
}
        

Kini mereka dapat disematkan ke dalam postingan (seperti di atas), digabungkan untuk memudahkan penjelajahan dan inspirasi, serta telah menambahkan kategori baru bagi kontributor lain untuk menambahkan polanya. Lihat keadaan sekitar, ambil beberapa kode: semuanya ada untuk Anda.

Ringkasan

Tiga kategori pola baru:

  1. Komponen
  2. Animasi
  3. Tema

Selain itu, lima pola baru ditambahkan ke pola Tata Letak yang sudah ada.

Komponen

Gambar pendukung yang memiliki komponen prototipe warna-warni dalam tata letak
petak.

Lihat halaman landing pola komponen atau periksa masing-masing satu per satu:

  1. Breadcrumb
  2. Tombol
  3. Carousel
  4. Dialog
  5. Menu Game
  6. Panel Pemuatan
  7. Scroller Media
  8. Pilihan Ganda
  9. Setelan
  10. Sidenav
  11. Tombol Terpisah
  12. Artikel
  13. favicon SVG
  14. Ganti
  15. Tab
  16. Toast

Berikut adalah pratinjau pola tombol terpisah:

HTML

<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>

CSS


        .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%;
  }
}
        

JS


        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)
})
        

Animasi

Gambar pendukung yang memiliki bola yang bergerak di kurva.

Lihat halaman landing pola animasi atau periksa satu per satu:

  1. Huruf Animasi
  2. Kata Animasi
  3. Surat Interaktif
  4. Kata Interaktif

Berikut pratinjau pola huruf animasi:

HTML

<h1 split-by="letter" letter-animation="breath">
  animated letters
</h1>

CSS


        @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;
  }
}
        

JS


        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)
  })
}
        

Tema

Gambar pendukung dengan dua lapisan dasbor, satu berwarna merah muda dan
yang lain berwarna biru.

Lihat halaman landing pola tema atau pelajari satu per satu:

  1. Skema Warna
  2. Tombol Tema

Salah satu polanya adalah untuk mem-build pengalihan tema sisi klien sehingga pengguna dapat menunjukkan preferensi mereka tanpa terikat langsung ke preferensi sistem. Kedua, untuk membuat sistem desain tema dengan properti kustom CSS.

Berikut pratinjau pola skema warna:

HTML

<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>

CSS


        * {
  /* 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);
}
        

JS


        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)
        

Pola tata letak baru pada pemusatan

Lihat halaman landing pola tata letak atau periksa satu per satu:

  1. Autobot
  2. Pusat Konten
  3. Fluffy Center
  4. Gentle Flex
  5. Pop n' Plop

Setiap demo memiliki tuas ambil untuk mengubah ukuran penampung dan tombol untuk menambahkan turunan ke tata letak. Hal ini, seperti yang dijelaskan dalam artikel ini, bermanfaat untuk membantu Anda merasakan kekuatan dan kelemahan berbagai teknik pemusatan yang ditawarkan web. Selain itu, nama-nama tersebut adalah nama yang menyenangkan.

Berikut adalah artikel yang menentukan "pemenang" eksplorasi pemusatan, Gentle Flex:

HTML

<article class="gentle-flex">
  <h1>Gentle Flex</h1>
</article>

CSS


        .gentle-flex {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1ch;
}
        

Penutup

Saya harap pola-pola baru ini dapat membantu mengajari Anda teknik-teknik baru, menginspirasi Anda, memberikan wawasan tentang aksesibilitas, dan secara keseluruhan membuat Anda bersemangat untuk membangun UI. Nantikan pola lainnya karena Tim Chrome terus menambahkan ke koleksi ini.