토스트 메시지 구성요소 빌드

적응형의 접근성 좋은 토스트 구성요소를 빌드하는 방법에 관한 기본 개요입니다.

이 게시물에서는 토스트 구성요소를 빌드하는 방법에 관한 생각을 공유하고자 합니다. 데모를 사용해 보세요.

데모

동영상을 선호하는 경우 이 게시물의 YouTube 버전을 확인하세요.

개요

토스트는 사용자를 위한 비대화형, 수동적, 비동기식 짧은 메시지입니다. 일반적으로 작업 결과를 사용자에게 알리는 인터페이스 피드백 패턴으로 사용됩니다.

상호작용 수

토스트는 알림, 경고, 프롬프트와 달리 상호작용이 불가능하며 닫거나 지속되도록 설계되지 않았습니다. 알림은 더 중요한 정보, 상호작용이 필요한 동기 메시지 또는 시스템 수준 메시지 (페이지 수준과 반대)에 사용됩니다. 토스트는 다른 알림 전략보다 수동적입니다.

마크업

<output> 요소는 스크린 리더에 공지되므로 토스트에 적합합니다. 올바른 HTML은 JavaScript와 CSS로 강화할 수 있는 안전한 기반을 제공하며 JavaScript가 많이 사용됩니다.

토스트

<output class="gui-toast">Item added to cart</output>

role="status"를 추가하면 더 포괄적일 수 있습니다. 이렇게 하면 브라우저가 사양에 따라 <output> 요소에 암시적 역할을 부여하지 않는 경우 대체가 제공됩니다.

<output role="status" class="gui-toast">Item added to cart</output>

토스트 컨테이너

한 번에 두 개 이상의 토스트를 표시할 수 있습니다. 여러 토스트를 조정하기 위해 컨테이너가 사용됩니다. 이 컨테이너는 화면에 표시되는 토스트의 위치도 처리합니다.

<section class="gui-toast-group">
  <output role="status">Wizard Rose added to cart</output>
  <output role="status">Self Watering Pot added to cart</output>
</section>

레이아웃

토스트를 뷰포트의 inset-block-end에 고정하기로 했으며, 토스트가 추가되면 해당 화면 가장자리에서 스택됩니다.

GUI 컨테이너

토스트 컨테이너는 토스트를 표시하기 위한 모든 레이아웃 작업을 실행합니다. 뷰포트에 fixed이고 논리 속성 inset를 사용하여 고정할 가장자리를 지정하며 동일한 block-end 가장자리에서 약간의 padding를 사용합니다.

.gui-toast-group {
  position: fixed;
  z-index: 1;
  inset-block-end: 0;
  inset-inline: 0;
  padding-block-end: 5vh;
}

.gui-toast-container 요소에 DevTools 상자 크기와 패딩이 오버레이된 스크린샷

토스트 컨테이너는 표시 영역 내에 배치될 뿐만 아니라 토스트를 정렬하고 분산할 수 있는 그리드 컨테이너입니다. 항목은 justify-content로 그룹화되어 중앙에 배치되고 justify-items로 개별적으로 중앙에 배치됩니다. 토스트가 서로 닿지 않도록 gap을 약간 넣습니다.

.gui-toast-group {
  display: grid;
  justify-items: center;
  justify-content: center;
  gap: 1vh;
}

이번에는 토스트 하위 요소 사이의 공간과 간격을 강조 표시한 토스트 그룹의 CSS 그리드 오버레이가 있는 스크린샷

GUI 토스트

개별 토스트에는 padding가 있고, border-radius로 부드러운 모서리가 있으며, 모바일 및 데스크톱 크기 조정을 지원하는 min() 기능이 있습니다. 다음 CSS의 반응형 크기는 토스트가 표시 영역의 90% 또는 25ch보다 넓어지는 것을 방지합니다.

.gui-toast {
  max-inline-size: min(25ch, 90vw);
  padding-block: .5ch;
  padding-inline: 1ch;
  border-radius: 3px;
  font-size: 1rem;
}

패딩과 테두리 반경이 표시된 단일 .gui-toast 요소의 스크린샷

스타일

레이아웃과 위치를 설정한 후 사용자 설정과 상호작용에 적응하는 데 도움이 되는 CSS를 추가합니다.

토스트 컨테이너

토스트는 대화형이 아니므로 탭하거나 스와이프해도 아무 작업도 실행되지 않지만 현재 포인터 이벤트를 소비합니다. 다음 CSS를 사용하여 토스트가 클릭을 가로채지 못하도록 합니다.

.gui-toast-group {
  pointer-events: none;
}

GUI 토스트

맞춤 속성, HSL, 환경설정 미디어 쿼리를 사용하여 토스트에 밝은 또는 어두운 적응형 테마를 적용합니다.

.gui-toast {
  --_bg-lightness: 90%;

  color: black;
  background: hsl(0 0% var(--_bg-lightness) / 90%);
}

@media (prefers-color-scheme: dark) {
  .gui-toast {
    color: white;
    --_bg-lightness: 20%;
  }
}

애니메이션

새 토스트는 화면에 표시될 때 애니메이션과 함께 표시되어야 합니다. 동작 줄이기를 수용하려면 기본적으로 translate 값을 0로 설정하되 동작 환경설정 미디어 쿼리에서 동작 값을 길이로 업데이트합니다 . 모든 사용자에게 애니메이션이 표시되지만 일부 사용자에게만 토스트가 이동합니다.

토스트 애니메이션에 사용된 키프레임은 다음과 같습니다. CSS는 하나의 애니메이션에서 토스트의 진입, 대기, 종료를 모두 제어합니다.

@keyframes fade-in {
  from { opacity: 0 }
}

@keyframes fade-out {
  to { opacity: 0 }
}

@keyframes slide-in {
  from { transform: translateY(var(--_travel-distance, 10px)) }
}

그러면 토스트 요소가 변수를 설정하고 키프레임을 오케스트레이션합니다.

.gui-toast {
  --_duration: 3s;
  --_travel-distance: 0;

  will-change: transform;
  animation: 
    fade-in .3s ease,
    slide-in .3s ease,
    fade-out .3s ease var(--_duration);
}

@media (prefers-reduced-motion: no-preference) {
  .gui-toast {
    --_travel-distance: 5vh;
  }
}

자바스크립트

스타일과 스크린 리더 접근 가능한 HTML이 준비되면 사용자 이벤트를 기반으로 토스트의 생성, 추가, 삭제를 조정하는 JavaScript가 필요합니다. 토스트 구성요소의 개발자 환경은 다음과 같이 최소화되어야 하며 시작하기 쉬워야 합니다.

import Toast from './toast.js'

Toast('My first toast')

토스트 그룹 및 토스트 만들기

토스트 모듈이 JavaScript에서 로드되면 토스트 컨테이너를 만들어 페이지에 추가해야 합니다. body 앞에 요소를 추가하여 z-index 스태킹 문제가 발생하지 않도록 했습니다. 컨테이너가 모든 본문 요소의 컨테이너 위에 있기 때문입니다.

const init = () => {
  const node = document.createElement('section')
  node.classList.add('gui-toast-group')

  document.firstElementChild.insertBefore(node, document.body)
  return node
}

head 태그와 body 태그 사이의 토스트 그룹 스크린샷

init() 함수는 모듈 내부에서 호출되어 요소를 Toaster로 저장합니다.

const Toaster = init()

토스트 HTML 요소 생성은 createToast() 함수를 사용하여 실행됩니다. 이 함수는 토스트에 필요한 텍스트를 가져와 <output> 요소를 만들고, 일부 클래스와 속성으로 장식하고, 텍스트를 설정하고, 노드를 반환합니다.

const createToast = text => {
  const node = document.createElement('output')
  
  node.innerText = text
  node.classList.add('gui-toast')
  node.setAttribute('role', 'status')

  return node
}

하나 또는 여러 토스트 관리

이제 JavaScript가 토스트를 포함하는 컨테이너를 문서에 추가하고 생성된 토스트를 추가할 준비가 되었습니다. addToast() 함수는 하나 또는 여러 개의 토스트 처리를 조정합니다. 먼저 토스트 수를 확인하고 동작이 괜찮은지 확인한 다음 이 정보를 사용하여 토스트를 추가하거나 다른 토스트가 새 토스트를 위해 '공간을 확보'하는 것처럼 보이도록 멋진 애니메이션을 실행합니다.

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

  Toaster.children.length && motionOK
    ? flipToast(toast)
    : Toaster.appendChild(toast)
}

첫 번째 토스트를 추가하면 Toaster.appendChild(toast)가 페이지에 토스트를 추가하여 CSS 애니메이션(애니메이션 인, 3s 대기, 애니메이션 아웃)을 트리거합니다. flipToast()는 기존 토스트가 있을 때 호출되며, Paul LewisFLIP이라는 기법을 사용합니다. 새 토스트가 추가되기 전후의 컨테이너 위치 차이를 계산하는 것이 아이디어입니다. 토스터가 현재 있는 위치와 이동할 위치를 표시한 다음 토스터가 있던 위치에서 현재 위치로 애니메이션을 적용하는 것과 같습니다.

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

CSS 그리드가 레이아웃을 처리합니다. 새 토스트가 추가되면 그리드는 토스트를 시작 부분에 배치하고 다른 토스트와 간격을 둡니다. 한편 웹 애니메이션은 이전 위치에서 컨테이너를 애니메이션화하는 데 사용됩니다.

모든 JavaScript 통합

Toast('my first toast')가 호출되면 토스트가 생성되어 페이지에 추가되고(새 토스트를 수용하기 위해 컨테이너가 애니메이션 처리될 수도 있음) Promise가 반환되며 생성된 토스트는 Promise 해결을 위해 CSS 애니메이션 완료 (3개의 키프레임 애니메이션)를 관찰합니다.

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

이 코드에서 혼동스러운 부분은 Promise.allSettled() 함수와 toast.getAnimations() 매핑이라고 생각합니다. 토스트에 여러 키프레임 애니메이션을 사용했으므로 모두 완료되었는지 확실히 알기 위해서는 JavaScript에서 각각 요청해야 하고 각각의 finished 약속이 완료되었는지 관찰해야 합니다. allSettled는 모든 프라미스가 충족되면 완료된 것으로 자체적으로 해결됩니다. await Promise.allSettled()를 사용하면 다음 코드 줄에서 요소를 삭제하고 토스트가 수명 주기를 완료했다고 가정할 수 있습니다. 마지막으로 resolve()를 호출하면 상위 수준 토스트 약속이 이행되므로 토스트가 표시된 후 개발자가 정리하거나 다른 작업을 할 수 있습니다.

export default Toast

마지막으로 Toast 함수는 다른 스크립트에서 가져와 사용할 수 있도록 모듈에서 내보내집니다.

Toast 구성요소 사용

토스트 또는 토스트의 개발자 환경을 사용하는 것은 Toast 함수를 가져와 메시지 문자열과 함께 호출하는 방식으로 이루어집니다.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

토스트가 표시된 후 정리 작업을 하려는 개발자는 비동기 및 await를 사용할 수 있습니다.

import Toast from './toast.js'

async function example() {
  await Toast('Wizard Rose added to cart')
  console.log('toast finished')
}

결론

이제 제가 어떻게 했는지 아셨으니, 어떻게 하시겠어요? 🙂

다양한 접근 방식을 사용하고 웹에서 빌드하는 모든 방법을 알아보세요. 데모를 만들고 트윗으로 링크를 보내주세요. 아래 커뮤니티 리믹스 섹션에 추가해 드리겠습니다.

커뮤니티 리믹스