Novos estampas

Animações, temas, componentes e mais padrões de layout estão ativos e prontos para ajudar a iniciar ou inspirar sua interface e UX.

Estou animado para compartilhar muitos novos padrões web.dev. Essas novidades são do programa Desafios da GUI, em que compartilho minhas estratégias sobre como criar vários componentes e necessidades comuns de interface, em seguida, coleto envios de usuários para as mesmas tarefas e nos ajuda a expandir nossas perspectivas sobre como resolvê-los.

O resultado é que os desafios da GUI se encaixam perfeitamente nos padrões:

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

Agora eles podem ser incorporados em postagens (como acima), agregados para facilitar a navegação e a inspiração, além de ter adicionado novas categorias para que outros colaboradores adicionem seus padrões. Dê uma olhada ao redor e pegue um código: está tudo lá para você.

Visão geral

Três novas categorias de padrões:

  1. Componentes
  2. Animações
  3. Temas

Além disso, cinco novos padrões foram adicionados aos padrões de Layout existentes.

Componentes

Gráfico de suporte que tem componentes prototipados coloridos em um layout
de grade.

Veja a página de destino dos padrões de componentes ou verifique cada uma individualmente:

  1. Navegação estrutural.
  2. botões
  3. Carrossel
  4. Diálogo
  5. Menu do jogo
  6. Barra de carregamento
  7. Rolador de mídia
  8. Seleção múltipla
  9. Configurações
  10. Navegação lateral
  11. Botões de divisão
  12. Stories
  13. Fávicon do SVG
  14. Switch
  15. Guias
  16. Toast

Esta é uma prévia do padrão do botão de divisão:

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

Animações

Imagem de apoio que mostra uma bola em movimento em uma curva.

Visualize a página de destino dos padrões de animação ou verifique cada uma individualmente:

  1. Cartas animadas
  2. Palavras animadas
  3. Cartas interativas
  4. Palavras interativas

Esta é uma visualização do padrão de letras animadas:

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

Temas

Gráfico de apoio com duas camadas de um painel, uma rosa e a outra azul.

Veja a página de destino dos padrões de temas ou confira cada um deles individualmente:

  1. Esquemas de cores
  2. Troca de tema

Um padrão é criar uma alternância de tema do lado do cliente para que os usuários possam indicar a preferência sem estar diretamente vinculada à preferência do sistema. O outro é criar um sistema de design de temas com propriedades personalizadas de CSS.

Esta é uma visualização do padrão de esquemas de cores:

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)
        

Novos padrões de layout na centralização

Confira a página de destino dos padrões de layout ou confira cada um deles individualmente:

  1. Autobot (link em inglês)
  2. Central de conteúdo
  3. Fluffy Center (link em inglês)
  4. Gentle Flex (link em inglês)
  5. Pop n' Plop

Cada demonstração tem uma alça para redimensionar o contêiner e um botão para adicionar um filho ao layout. Elas, conforme explicado no artigo, ajudam você a sentir os pontos fortes e fracos das várias técnicas de centralização oferecidas pela Web. Além disso, eles têm nomes divertidos.

Este é o artigo determinado como o "vencedor" da exploração central, o 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;
}
        

Conclusão

Espero que esses novos padrões ajudem a ensinar novas técnicas, inspire você, forneçam insights sobre acessibilidade e mantenham você envolvido na criação de interfaces. Fique atento a mais padrões à medida que a equipe do Chrome continua a adicionar itens a essas coleções.