Skillversitas

CSS Animations

CSS animations allow you to create more complex animations than transitions. With @keyframes, you can define multiple change points (keyframes) along an animation timeline — not just from an initial state to a final state like transitions.

The main differences from transitions:

  • Transitions require a trigger (hover, focus, class change)
  • Animations can run automatically without a trigger
  • Animations support multiple keyframes (not just start-end)
  • Animations can repeat (loop), run back and forth (alternate), and be delayed

The main components:

  • @keyframes — defines the change points
  • animation-name — the name of the keyframes to use
  • animation-duration — the duration of one animation cycle
  • animation-timing-function — the speed curve
  • animation-delay — the delay before the animation starts
  • animation-iteration-count — how many times the animation repeats
  • animation-direction — the direction of the animation
  • animation-fill-mode — the element state before/after the animation

Code Example

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CSS Animations</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background: #0f0f1a;
      color: #fff;
      padding: 40px 20px;
    }

    h1 {
      text-align: center;
      margin-bottom: 10px;
    }

    .subtitle {
      text-align: center;
      color: #888;
      margin-bottom: 50px;
    }

    .section {
      max-width: 800px;
      margin: 0 auto 60px;
    }

    .section h2 {
      text-align: center;
      margin-bottom: 25px;
      color: #667eea;
    }

    .demo-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
      gap: 30px;
    }

    .demo-item {
      text-align: center;
    }

    .demo-item h4 {
      margin-bottom: 15px;
      font-size: 0.8rem;
      color: #aaa;
    }

    /* ===== Loading Spinner ===== */
    @keyframes spin {
      from { transform: rotate(0deg); }
      to { transform: rotate(360deg); }
    }

    @keyframes pulse-opacity {
      0%, 100% { opacity: 1; }
      50% { opacity: 0.3; }
    }

    .spinner {
      width: 50px;
      height: 50px;
      border: 4px solid rgba(102, 126, 234, 0.2);
      border-top-color: #667eea;
      border-radius: 50%;
      margin: 0 auto;
      animation: spin 1s linear infinite;
    }

    .spinner-dots {
      display: flex;
      gap: 8px;
      justify-content: center;
    }

    .spinner-dot {
      width: 14px;
      height: 14px;
      background: #667eea;
      border-radius: 50%;
      animation: pulse-opacity 1.4s ease-in-out infinite;
    }

    .spinner-dot:nth-child(2) { animation-delay: 0.2s; }
    .spinner-dot:nth-child(3) { animation-delay: 0.4s; }

    /* ===== Bounce Effect ===== */
    @keyframes bounce {
      0%, 100% { transform: translateY(0); }
      40% { transform: translateY(-30px); }
      60% { transform: translateY(-15px); }
    }

    @keyframes bounce-ball {
      0%, 100% {
        transform: translateY(0) scaleY(1);
      }
      30% {
        transform: translateY(-40px) scaleY(1);
      }
      50% {
        transform: translateY(0) scaleY(0.8) scaleX(1.2);
      }
      65% {
        transform: translateY(-15px) scaleY(1);
      }
      80% {
        transform: translateY(0) scaleY(0.9) scaleX(1.1);
      }
    }

    .bounce-text {
      font-size: 2rem;
      font-weight: bold;
      color: #f5576c;
      display: inline-block;
      animation: bounce 2s ease infinite;
    }

    .bounce-ball {
      width: 40px;
      height: 40px;
      background: #43e97b;
      border-radius: 50%;
      margin: 0 auto;
      animation: bounce-ball 1.5s ease infinite;
    }

    /* ===== Slide-In ===== */
    @keyframes slideInLeft {
      from {
        transform: translateX(-100%);
        opacity: 0;
      }
      to {
        transform: translateX(0);
        opacity: 1;
      }
    }

    @keyframes slideInRight {
      from {
        transform: translateX(100%);
        opacity: 0;
      }
      to {
        transform: translateX(0);
        opacity: 1;
      }
    }

    @keyframes slideInUp {
      from {
        transform: translateY(50px);
        opacity: 0;
      }
      to {
        transform: translateY(0);
        opacity: 1;
      }
    }

    .slide-demo {
      display: flex;
      gap: 15px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .slide-box {
      padding: 15px 25px;
      border-radius: 8px;
      font-weight: 600;
      font-size: 0.85rem;
    }

    .slide-left {
      background: #667eea;
      animation: slideInLeft 0.8s ease-out forwards;
    }

    .slide-right {
      background: #f5576c;
      animation: slideInRight 0.8s ease-out 0.2s forwards;
      opacity: 0;
    }

    .slide-up {
      background: #43e97b;
      color: #1a1a2e;
      animation: slideInUp 0.8s ease-out 0.4s forwards;
      opacity: 0;
    }

    /* ===== Pulse ===== */
    @keyframes pulse {
      0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.7); }
      70% { transform: scale(1.05); box-shadow: 0 0 0 20px rgba(102, 126, 234, 0); }
      100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(102, 126, 234, 0); }
    }

    .pulse-btn {
      width: 120px;
      height: 120px;
      border-radius: 50%;
      background: #667eea;
      color: white;
      border: none;
      font-size: 0.9rem;
      font-weight: 600;
      cursor: pointer;
      animation: pulse 2s ease-in-out infinite;
    }

    /* ===== Typing Effect ===== */
    @keyframes blink-caret {
      from, to { border-color: transparent; }
      50% { border-color: #667eea; }
    }

    @keyframes type {
      from { width: 0; }
      to { width: 100%; }
    }

    .typing-container {
      display: flex;
      justify-content: center;
    }

    .typing-text {
      font-family: 'Fira Code', monospace;
      font-size: 1.2rem;
      color: #667eea;
      border-right: 3px solid #667eea;
      white-space: nowrap;
      overflow: hidden;
      width: 0;
      animation:
        type 3s steps(30, end) forwards,
        blink-caret 0.75s step-end infinite;
    }

    /* ===== Direction Demo ===== */
    @keyframes moveRight {
      0% { transform: translateX(0); }
      100% { transform: translateX(150px); }
    }

    .direction-track {
      width: 200px;
      height: 40px;
      background: rgba(255,255,255,0.05);
      border-radius: 20px;
      margin: 0 auto 15px;
      position: relative;
      overflow: hidden;
    }

    .direction-ball {
      width: 30px;
      height: 30px;
      background: #667eea;
      border-radius: 50%;
      position: absolute;
      top: 5px;
      left: 5px;
    }

    .dir-normal .direction-ball { animation: moveRight 2s ease infinite normal; }
    .dir-reverse .direction-ball { animation: moveRight 2s ease infinite reverse; }
    .dir-alternate .direction-ball { animation: moveRight 2s ease infinite alternate; }
    .dir-alternate-reverse .direction-ball { animation: moveRight 2s ease infinite alternate-reverse; }

    /* ===== Fill Mode Demo ===== */
    @keyframes fadeSlide {
      from {
        transform: translateY(20px);
        opacity: 0;
      }
      to {
        transform: translateY(0);
        opacity: 1;
      }
    }

    .fill-grid {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 20px;
    }

    .fill-item {
      background: rgba(102, 126, 234, 0.15);
      border: 1px solid rgba(102, 126, 234, 0.3);
      border-radius: 8px;
      padding: 20px;
      text-align: center;
    }

    .fill-item h4 {
      font-size: 0.75rem;
      color: #667eea;
      margin-bottom: 10px;
    }

    .fill-none {
      animation: fadeSlide 0.5s ease none;
    }

    .fill-forwards {
      animation: fadeSlide 0.5s ease forwards;
    }

    .fill-backwards {
      animation: fadeSlide 0.5s ease backwards;
    }

    .fill-both {
      animation: fadeSlide 0.5s ease both;
    }

    .fill-item p {
      font-size: 0.8rem;
      color: #ccc;
    }

    /* ===== Practical Examples ===== */
    @keyframes gradientShift {
      0% { background-position: 0% 50%; }
      50% { background-position: 100% 50%; }
      100% { background-position: 0% 50%; }
    }

    @keyframes shake {
      0%, 100% { transform: translateX(0); }
      10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
      20%, 40%, 60%, 80% { transform: translateX(5px); }
    }

    @keyframes float {
      0%, 100% { transform: translateY(0px); }
      50% { transform: translateY(-20px); }
    }

    @keyframes ripple {
      0% { transform: scale(0); opacity: 1; }
      100% { transform: scale(4); opacity: 0; }
    }

    .practical-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 30px;
    }

    .practical-item {
      text-align: center;
    }

    .practical-item h4 {
      margin-bottom: 15px;
      font-size: 0.8rem;
      color: #aaa;
    }

    .gradient-box {
      width: 150px;
      height: 150px;
      border-radius: 16px;
      background: linear-gradient(-45deg, #667eea, #764ba2, #f5576c, #4facfe);
      background-size: 400% 400%;
      animation: gradientShift 4s ease infinite;
      margin: 0 auto;
    }

    .shake-text {
      font-size: 1.5rem;
      font-weight: bold;
      color: #f5576c;
      display: inline-block;
      cursor: pointer;
    }

    .shake-text:hover {
      animation: shake 0.5s ease;
    }

    .float-element {
      font-size: 3rem;
      animation: float 3s ease-in-out infinite;
    }

    .ripple-btn {
      width: 120px;
      height: 50px;
      background: #667eea;
      color: white;
      border: none;
      border-radius: 8px;
      font-weight: 600;
      cursor: pointer;
      position: relative;
      overflow: hidden;
      margin: 0 auto;
    }

    .ripple-btn::after {
      content: '';
      position: absolute;
      width: 50px;
      height: 50px;
      background: rgba(255,255,255,0.3);
      border-radius: 50%;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%) scale(0);
      opacity: 0;
    }

    .ripple-btn:active::after {
      animation: ripple 0.6s ease-out;
    }

    /* ===== Animation Properties Table ===== */
    .props-table {
      width: 100%;
      border-collapse: collapse;
      margin-top: 20px;
    }

    .props-table th,
    .props-table td {
      padding: 12px 16px;
      text-align: left;
      border-bottom: 1px solid rgba(255,255,255,0.1);
      font-size: 0.85rem;
    }

    .props-table th {
      color: #667eea;
      font-weight: 600;
    }

    .props-table td:first-child {
      font-family: monospace;
      color: #43e97b;
    }

    .code-example {
      background: #1a1a2e;
      border-radius: 12px;
      padding: 25px;
      margin-top: 30px;
      font-family: monospace;
      font-size: 0.85rem;
      line-height: 1.6;
      color: #e0e0e0;
      overflow-x: auto;
    }

    .code-example .keyword { color: #c792ea; }
    .code-example .property { color: #82aaff; }
    .code-example .value { color: #c3e88d; }
    .code-example .comment { color: #546e7a; }
  </style>
</head>
<body>

  <h1>CSS Animations</h1>
  <p class="subtitle">Automatic animations without JavaScript using @keyframes</p>

  <div class="section">
    <h2>Loading Spinner</h2>
    <div class="demo-grid">
      <div class="demo-item">
        <h4>Rotating Spinner</h4>
        <div class="spinner"></div>
      </div>
      <div class="demo-item">
        <h4>Pulsing Dots</h4>
        <div class="spinner-dots">
          <div class="spinner-dot"></div>
          <div class="spinner-dot"></div>
          <div class="spinner-dot"></div>
        </div>
      </div>
    </div>
  </div>

  <div class="section">
    <h2>Bounce Effect</h2>
    <div class="demo-grid">
      <div class="demo-item">
        <h4>Bouncing Text</h4>
        <div class="bounce-text">HOI!</div>
      </div>
      <div class="demo-item">
        <h4>Bouncing Ball</h4>
        <div class="bounce-ball"></div>
      </div>
    </div>
  </div>

  <div class="section">
    <h2>Slide-In Animation</h2>
    <div class="slide-demo">
      <div class="slide-box slide-left">← Left</div>
      <div class="slide-box slide-right">Right →</div>
      <div class="slide-box slide-up">↑ Up</div>
    </div>
  </div>

  <div class="section">
    <h2>Pulse Effect</h2>
    <div class="demo-item">
      <h4>Click the Button</h4>
      <button class="pulse-btn">Pulse</button>
    </div>
  </div>

  <div class="section">
    <h2>Typing Effect</h2>
    <div class="typing-container">
      <div class="typing-text">Welcome to CSS Animations!</div>
    </div>
  </div>

  <div class="section">
    <h2>Animation Direction</h2>
    <div class="demo-grid">
      <div class="demo-item">
        <h4>normal</h4>
        <div class="direction-track dir-normal">
          <div class="direction-ball"></div>
        </div>
      </div>
      <div class="demo-item">
        <h4>reverse</h4>
        <div class="direction-track dir-reverse">
          <div class="direction-ball"></div>
        </div>
      </div>
      <div class="demo-item">
        <h4>alternate</h4>
        <div class="direction-track dir-alternate">
          <div class="direction-ball"></div>
        </div>
      </div>
      <div class="demo-item">
        <h4>alternate-reverse</h4>
        <div class="direction-track dir-alternate-reverse">
          <div class="direction-ball"></div>
        </div>
      </div>
    </div>
  </div>

  <div class="section">
    <h2>Animation Fill Mode</h2>
    <div class="fill-grid">
      <div class="fill-item">
        <h4>none</h4>
        <div class="fill-none" >Text</div>
        <p>Reset to the initial state after finishing</p>
      </div>
      <div class="fill-item">
        <h4>forwards</h4>
        <div class="fill-forwards" >Text</div>
        <p>Retain the final state of the animation</p>
      </div>
      <div class="fill-item">
        <h4>backwards</h4>
        <div class="fill-backwards" >Text</div>
        <p>Apply the initial state during the delay</p>
      </div>
      <div class="fill-item">
        <h4>both</h4>
        <div class="fill-both" >Text</div>
        <p>Kombinasi forwards + backwards</p>
      </div>
    </div>
  </div>

  <div class="section">
    <h2>Everyday Practice</h2>
    <div class="practical-grid">
      <div class="practical-item">
        <h4>Gradient Background Shift</h4>
        <div class="gradient-box"></div>
      </div>
      <div class="practical-item">
        <h4>Error Shake</h4>
        <div class="shake-text">EROOR!</div>
        <p style="font-size:0.8rem;color:#888;margin-top:10px;">hover for the shake effect</p>
      </div>
      <div class="practical-item">
        <h4>Floating Element</h4>
        <div class="float-element">🎈</div>
      </div>
      <div class="practical-item">
        <h4>Ripple Button</h4>
        <button class="ripple-btn">Click Me</button>
        <p style="font-size:0.8rem;color:#888;margin-top:10px;">click for the ripple effect</p>
      </div>
    </div>
  </div>

  <div class="section">
    <h2>Referensi Properti</h2>
    <table class="props-table">
      <thead>
        <tr>
          <th>Properti</th>
          <th>Default Value</th>
          <th>Description</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>animation-name</td>
          <td>none</td>
          <td>The name of the keyframes to use</td>
        </tr>
        <tr>
          <td>animation-duration</td>
          <td>0s</td>
          <td>Duration of one animation cycle</td>
        </tr>
        <tr>
          <td>animation-timing-function</td>
          <td>ease</td>
          <td>Animation speed curve</td>
        </tr>
        <tr>
          <td>animation-delay</td>
          <td>0s</td>
          <td>Delay time before the animation starts</td>
        </tr>
        <tr>
          <td>animation-iteration-count</td>
          <td>1</td>
          <td>Number of repetitions (a number or infinite)</td>
        </tr>
        <tr>
          <td>animation-direction</td>
          <td>normal</td>
          <td>Direction: normal, reverse, alternate, alternate-reverse</td>
        </tr>
        <tr>
          <td>animation-fill-mode</td>
          <td>none</td>
          <td>State before/after: none, forwards, backwards, both</td>
        </tr>
        <tr>
          <td>animation-play-state</td>
          <td>running</td>
          <td>running or paused</td>
        </tr>
      </tbody>
    </table>
  </div>

  <div class="code-example">
    <span class="comment">/* Shorthand: name duration timing delay count direction fill */</span><br>
    <span class="property">animation</span>: <span class="value">fadeIn 0.5s ease 0.2s 1 normal forwards</span>;<br><br>
    <span class="comment">/* Example: loading spinner */</span><br>
    <span class="keyword">@keyframes</span> <span class="value">spin</span> {<br>
    &nbsp;&nbsp;<span class="property">from</span> { <span class="property">transform</span>: <span class="value">rotate(0deg)</span>; }<br>
    &nbsp;&nbsp;<span class="property">to</span> { <span class="property">transform</span>: <span class="value">rotate(360deg)</span>; }<br>
    }<br><br>
    <span class="property">.spinner</span> {<br>
    &nbsp;&nbsp;<span class="property">animation</span>: <span class="value">spin 1s linear infinite</span>;<br>
    }
  </div>

</body>
</html>

Code Explanation

@keyframes

@keyframes defines change points along the animation timeline. You can use from/to (equivalent to 0%/100%) or specific percentages for multiple points.

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes colorShift {
  0%   { background: red; }
  33%  { background: yellow; }
  66%  { background: green; }
  100% { background: red; }
}

animation-name

Connects an element to keyframes that have been defined.

.box {
  animation-name: fadeIn;
}

animation-duration

Determines how long one animation cycle lasts. It must be greater than 0.

.box {
  animation-duration: 2s;    /* 2 seconds per cycle */
  animation-duration: 500ms; /* 500 milliseconds */
}

animation-delay

The delay before the animation starts. It can be negative to "start" the animation partway through.

.box {
  animation-delay: 0.5s;   /* wait 0.5 seconds */
  animation-delay: -1s;    /* start from the 1st second */
}

animation-iteration-count

How many times the animation repeats. It can be a number or infinite.

.box {
  animation-iteration-count: 3;    /* repeat 3 times */
  animation-iteration-count: infinite; /* repeat continuously */
}

animation-direction

Controls the direction of the animation:

  • normal — 0% to 100%, then restart from 0%
  • reverse — 100% to 0%, then restart from 100%
  • alternate — 0% to 100%, then 100% back to 0% (back and forth)
  • alternate-reverse — the reverse of alternate
.box {
  animation-direction: alternate; /* back and forth */
}

animation-fill-mode

Controls the element state before and after the animation:

  • none — the element returns to its original CSS state
  • forwards — keep the final state of the animation
  • backwards — apply the initial state during the delay
  • both — a combination of forwards + backwards
.box {
  animation-fill-mode: forwards; /* stay in the final state */
}

animation Shorthand

/* animation: name duration timing-function delay iteration-count direction fill-mode play-state; */
.box {
  animation: fadeIn 0.5s ease 0s 1 normal forwards;
}

/* Can have multiple animations */
.box {
  animation: fadeIn 0.5s ease, slideUp 0.8s ease 0.3s;
}

Percentages in @keyframes

You can use percentages to control the timing of changes with more precision:

@keyframes complex {
  0%   { transform: translateX(0); opacity: 0; }
  20%  { transform: translateX(50px); opacity: 1; }
  50%  { transform: translateX(100px); }
  80%  { transform: translateX(50px); opacity: 0.5; }
  100% { transform: translateX(0); opacity: 1; }
}

Everyday Use Cases

1. Loading Spinner on a Website

Every modern website uses a spinner while content loads. CSS animation enables continuous rotation without JavaScript:

@keyframes spin {
  to { transform: rotate(360deg); }
}

.loader {
  width: 40px;
  height: 40px;
  border: 3px solid #eee;
  border-top-color: #667eea;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

2. Error Shake on a Form

When a user fills out a form incorrectly, the element shakes to indicate the error:

@keyframes shake {
  0%, 100% { transform: translateX(0); }
  20%, 60% { transform: translateX(-8px); }
  40%, 80% { transform: translateX(8px); }
}

.input-error {
  border-color: red;
  animation: shake 0.4s ease;
}

3. Hero Section Background

A slowly moving gradient background in the hero section gives a modern and dynamic impression:

@keyframes gradientMove {
  0%   { background-position: 0% 50%; }
  50%  { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

.hero {
  background: linear-gradient(-45deg, #667eea, #764ba2, #f5576c, #4facfe);
  background-size: 400% 400%;
  animation: gradientMove 10s ease infinite;
}

4. Notification Badge Pulse

A notification badge that pulses to catch the user's attention:

@keyframes badgePulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.2); }
}

.notification-badge {
  animation: badgePulse 2s ease-in-out infinite;
}

5. Page Transition

A fade-in animation when a new page loads — providing a smooth transition between pages:

@keyframes pageIn {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.page-content {
  animation: pageIn 0.6s ease forwards;
}

Conclusion

CSS animations give you full control over complex animations through @keyframes and the various animation properties. @keyframes defines the change points, animation-iteration-count: infinite makes an animation repeat forever, animation-direction: alternate makes it run back and forth, and animation-fill-mode: forwards keeps the final state. CSS animations are more powerful than transitions because they support multiple keyframes, run automatically without a trigger, and can loop indefinitely. Use transitions for simple state-based animations, and animations for complex ones that run automatically.