Animations

htmxλŠ” CSS μ „ν™˜μ„ μ‚¬μš©ν•˜μ—¬ CSS와 HTML만으둜 μ›Ή νŽ˜μ΄μ§€μ— λΆ€λ“œλŸ¬μš΄ μ• λ‹ˆλ©”μ΄μ…˜κ³Ό μ „ν™˜ 효과λ₯Ό μΆ”κ°€ν•  수 μžˆλ„λ‘ μ„€κ³„λ˜μ—ˆμŠ΅λ‹ˆλ‹€. μ•„λž˜μ—λŠ” λ‹€μ–‘ν•œ μ• λ‹ˆλ©”μ΄μ…˜ κΈ°λ²•μ˜ λͺ‡ 가지 μ˜ˆμ‹œκ°€ λ‚˜μ™€ μžˆμŠ΅λ‹ˆλ‹€.

λ˜ν•œ htmxλŠ” μƒˆλ‘œμš΄ View Transitions APIλ₯Ό μ‚¬μš©ν•˜μ—¬ μ• λ‹ˆλ©”μ΄μ…˜μ„ 생성할 수 μžˆλ„λ‘ ν•©λ‹ˆλ‹€.

#κΈ°λ³Έ CSS μ• λ‹ˆλ©”μ΄μ…˜

#색상 λ³€ν™”

htmxμ—μ„œ κ°€μž₯ κ°„λ‹¨ν•œ μ• λ‹ˆλ©”μ΄μ…˜ 기법은 μ½˜ν…μΈ  ꡐ체 쀑에 μš”μ†Œμ˜ idλ₯Ό μœ μ§€ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. μš”μ†Œμ˜ idκ°€ μœ μ§€λ˜λ©΄, htmxλŠ” CSS μ „ν™˜μ„ 톡해 ν•΄λ‹Ή μš”μ†Œμ˜ 이전 버전과 μƒˆ 버전 사이에 μ „ν™˜ 효과λ₯Ό μž‘μ„±ν•  수 μžˆλ„λ‘ ꡐ체λ₯Ό κ΅¬μ‘°ν™”ν•©λ‹ˆλ‹€.

λ‹€μŒμ˜ divλ₯Ό 예둜 λ“€μ–΄λ³΄κ² μŠ΅λ‹ˆλ‹€:

<style>
.smooth {
  transition: all 1s ease-in;
}
</style>
<div id="color-demo" class="smooth" style="color:red"
      hx-get="/colors" hx-swap="outerHTML" hx-trigger="every 1s">
  Color Swap Demo
</div>

이 divλŠ” λ§€μ΄ˆλ§ˆλ‹€ 폴링(polling)ν•˜λ©° color μŠ€νƒ€μΌμ„ μƒˆλ‘œμš΄ κ°’(예: blue)으둜 λ³€κ²½ν•˜λŠ” μƒˆλ‘œμš΄ μ½˜ν…μΈ λ‘œ κ΅μ²΄λ©λ‹ˆλ‹€:

<div id="color-demo" class="smooth" style="color:blue"
      hx-get="/colors" hx-swap="outerHTML" hx-trigger="every 1s">
  Color Swap Demo
</div>

이 divλŠ” color-demoλΌλŠ” μ•ˆμ •μ μΈ idλ₯Ό 가지고 있기 λ•Œλ¬Έμ—, htmxλŠ” ꡐ체λ₯Ό κ΅¬μ‘°ν™”ν•˜μ—¬ .smooth ν΄λž˜μŠ€μ— μ •μ˜λœ CSS μ „ν™˜μ΄ redμ—μ„œ blue둜 μŠ€νƒ€μΌ μ—…λ°μ΄νŠΈμ— 적용되고, 두 색상 사이λ₯Ό λΆ€λ“œλŸ½κ²Œ μ „ν™˜ν•  수 μžˆλ„λ‘ ν•©λ‹ˆλ‹€.

#Demo

Color Swap Demo

#Smooth Progress Bar

진행λ₯  ν‘œμ‹œμ€„ 데λͺ¨μ—μ„œλŠ” 진행λ₯  ν‘œμ‹œμ€„ μš”μ†Œμ˜ length 속성을 μ—…λ°μ΄νŠΈν•˜μ—¬ λΆ€λ“œλŸ¬μš΄ μ• λ‹ˆλ©”μ΄μ…˜μ„ κ΅¬ν˜„ν•˜λŠ” 이 κΈ°λ³Έ CSS μ• λ‹ˆλ©”μ΄μ…˜ 기법도 μ‚¬μš©ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

#Swap Transitions

#Fade Out On Swap

μš”μ²­μ΄ μ’…λ£Œλ  λ•Œ 제거될 μš”μ†Œλ₯Ό νŽ˜μ΄λ“œ μ•„μ›ƒν•˜λ €λ©΄ 일뢀 CSSλ₯Ό μ‚¬μš©ν•˜μ—¬ htmx-swapping 클래슀λ₯Ό ν™œμš©ν•˜κ³  μ• λ‹ˆλ©”μ΄μ…˜μ΄ μ™„λ£Œλ  λ•ŒκΉŒμ§€ ꡐ체 단계λ₯Ό μΆ©λΆ„νžˆ 길게 μ—°μž₯ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
μ΄λ ‡κ²Œ ν•˜λ©΄ λ©λ‹ˆλ‹€:

<style>
.fade-me-out.htmx-swapping {
  opacity: 0;
  transition: opacity 1s ease-out;
}
</style>
<button class="fade-me-out"
        hx-delete="/fade_out_demo"
        hx-swap="outerHTML swap:1s">
        Fade Me Out
</button>

#Demo

#Settling Transitions

#Fade In On Addition

λ§ˆμ§€λ§‰ 예제λ₯Ό 기반으둜 μ„€μ • λ‹¨κ³„μ—μ„œ htmx-added 클래슀λ₯Ό μ‚¬μš©ν•˜μ—¬ μƒˆ μ½˜ν…μΈ λ₯Ό νŽ˜μ΄λ“œ 인할 수 μžˆμŠ΅λ‹ˆλ‹€.
μƒˆ μ½˜ν…μΈ κ°€ μ•„λ‹Œ 타깃에 λŒ€ν•œ CSS Transition을 htmx-settling 클래슀λ₯Ό μ‚¬μš©ν•˜μ—¬ μž‘μ„±ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.

<style>
#fade-me-in.htmx-added {
  opacity: 0;
}
#fade-me-in {
  opacity: 1;
  transition: opacity 1s ease-out;
}
</style>
<button id="fade-me-in"
        class="btn primary"
        hx-post="/fade_in_demo"
        hx-swap="outerHTML settle:1s">
        Fade Me In
</button>

#Demo

#Request In Flight Animation

μš”μ²­μ„ νŠΈλ¦¬κ±°ν•˜λŠ” μš”μ†Œμ— μ μš©λ˜λŠ” htmx-request 클래슀λ₯Ό ν™œμš©ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.
μ•„λž˜λŠ” 제좜 μ‹œ μš”μ²­μ΄ 처리 μ€‘μž„μ„ λ‚˜νƒ€λ‚΄κΈ° μœ„ν•΄ λͺ¨μ–‘이 λ³€κ²½λ˜λŠ” Formμž…λ‹ˆλ‹€:

<style>
  form.htmx-request {
    opacity: .5;
    transition: opacity 300ms linear;
  }
</style>
<form hx-post="/name" hx-swap="outerHTML">
<label>Name:</label><input name="name"><br/>
<button class="btn primary">Submit</button>
</form>

#Demo


#Using the htmx class-tools Extension

class-tools ν™•μž₯을 μ‚¬μš©ν•˜λ©΄ ν₯미둜운 μ• λ‹ˆλ©”μ΄μ…˜μ„ 많이 λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€.

λ‹€μŒμ€ div의 뢈투λͺ…도λ₯Ό ν† κΈ€ν•˜λŠ” μ˜ˆμ œμž…λ‹ˆλ‹€. ν† κΈ€ μ‹œκ°„μ„ μ „ν™˜ μ‹œκ°„λ³΄λ‹€ μ•½κ°„ 길게 μ„€μ •ν•œ 것을 μ£Όλͺ©ν•˜μ„Έμš”.
μ΄λ ‡κ²Œ ν•˜λ©΄ 클래슀 λ³€κ²½μœΌλ‘œ 인해 μ „ν™˜μ΄ 쀑단될 λ•Œ λ°œμƒν•  수 μžˆλŠ” κΉœλ°•μž„μ„ 방지할 수 μžˆμŠ΅λ‹ˆλ‹€.

<style>
.demo.faded {
  opacity:.3;
}
.demo {
  opacity:1;
  transition: opacity ease-in 900ms;
}
</style>
<div class="demo" classes="toggle faded:1s">Toggle Demo</div>

#Demo

Toggle Demo

#Using the View Transition API

htmxλŠ” hx-swap μ†μ„±μ˜ transition μ˜΅μ…˜μ„ 톡해 μƒˆλ‘œμš΄ View Transitions API에 μ ‘κ·Όν•  수 μžˆλ„λ‘ μ§€μ›ν•©λ‹ˆλ‹€.

μ•„λž˜λŠ” λ·° μ „ν™˜μ„ μ‚¬μš©ν•˜λŠ” ꡐ체(swap)의 μ˜ˆμ‹œμž…λ‹ˆλ‹€. 이 μ „ν™˜μ€ CSSμ—μ„œ view-transition-name 속성을 톡해 μ™ΈλΆ€ div에 μ—°κ²°λ˜λ©°, ν•΄λ‹Ή μ „ν™˜μ€ ::view-transition-old와 ::view-transition-newλ₯Ό μ‚¬μš©ν•˜μ—¬ μ •μ˜λ˜λ©°, @keyframesλ₯Ό 톡해 μ• λ‹ˆλ©”μ΄μ…˜μ„ μ •μ˜ν•©λ‹ˆλ‹€. (View Transition API에 λŒ€ν•œ μžμ„Έν•œ λ‚΄μš©μ€ Chrome Developer Pageμ—μ„œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.)

이 μ „ν™˜μ˜ 이전 μ½˜ν…μΈ λŠ” μ™Όμͺ½μœΌλ‘œ μŠ¬λΌμ΄λ“œ μ•„μ›ƒλ˜κ³ , μƒˆλ‘œμš΄ μ½˜ν…μΈ λŠ” 였λ₯Έμͺ½μ—μ„œ μŠ¬λΌμ΄λ“œ μΈλ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.

μž‘μ„± μ‹œμ μ—μ„œ 이 μ‹œκ°μ  μ „ν™˜μ€ Chrome 111+μ—μ„œλ§Œ λ°œμƒν•˜μ§€λ§Œ, κ°€κΉŒμš΄ λ―Έλž˜μ— 더 λ§Žμ€ λΈŒλΌμš°μ €μ—μ„œ 이 κΈ°λŠ₯을 κ΅¬ν˜„ν•  κ²ƒμœΌλ‘œ μ˜ˆμƒλ©λ‹ˆλ‹€.

<style>
   @keyframes fade-in {
     from { opacity: 0; }
   }

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

   @keyframes slide-from-right {
     from { transform: translateX(90px); }
   }

   @keyframes slide-to-left {
     to { transform: translateX(-90px); }
   }

   .slide-it {
     view-transition-name: slide-it;
   }

   ::view-transition-old(slide-it) {
     animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
     600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
   }
   ::view-transition-new(slide-it) {
     animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
     600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
   }
</style>


<div class="slide-it">
   <h1>Initial Content</h1>
   <button class="btn primary" hx-get="/new-content" hx-swap="innerHTML transition:true" hx-target="closest div">
     Swap It!
   </button>
</div>

#Demo

Initial Content

#Conclusion

μœ„μ˜ 기법을 μ‚¬μš©ν•˜λ©΄ htmxλ₯Ό μ‚¬μš©ν•˜λ©΄μ„œ 일반 HTML둜 ν₯λ―Έλ‘­κ³  즐거운 효과λ₯Ό κ½€ 많이 λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€.