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

적응형이며 액세스 가능한 토스트 구성요소를 빌드하는 방법에 관한 기본 개요입니다.

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

데모

동영상을 선호하는 경우 이 게시물의 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;
}

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

토스트 컨테이너는 표시 영역 내에 배치하는 것 외에도 토스트를 정렬하고 배포할 수 있는 그리드 컨테이너입니다. 항목은 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()

Toast 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()는 기존 토스트가 있을 때 호출되며 폴 루이스FLIP이라는 기법을 사용합니다. 새 토스트가 추가되기 전과 후의 컨테이너 위치 차이를 계산하는 것입니다. 토스터가 현재 어디에 있고 어디로 이동할지 표시한 다음 이전 위치에서 현재 위치로 애니메이션을 적용하는 것과 같습니다.

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 애니메이션 완료 (세 개의 키프레임 애니메이션)를 감시합니다.

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()를 호출하면 상위 수준의 Toast 약속이 실행되므로 개발자는 토스트가 표시된 후 정리하거나 다른 작업을 할 수 있습니다.

export default Toast

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

Toast 구성요소 사용

토스트 또는 토스트의 개발자 환경을 사용하려면 Toast 함수를 가져와 메시지 문자열로 호출하면 됩니다.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

개발자가 토스트가 표시된 후 정리 작업 등을 수행하려면 async 및 await를 사용하면 됩니다.

import Toast from './toast.js'

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

결론

이제 제가 어떻게 했는지 알았으니 어떻게 하시겠어요? 🙂

접근 방식을 다양화하고 웹에서 빌드하는 모든 방법을 알아보겠습니다. 데모를 만들어 트윗해 주시면 아래의 커뮤니티 리믹스 섹션에 추가해 드리겠습니다.

커뮤니티 리믹스