Skip to content
📖 7 min🎯Difficulty:
Prerequisites:Transitions
You'll build:An animated loading indicator and pulsing notification dot

The Problem

Transitions handle state changes, but not all animations are state-driven. A loading spinner spins continuously. A notification dot pulses when a new alert arrives. A volume bar animates smoothly when you change the level. These are standalone animations — they run on their own schedule, not in response to state changes.

The Naive Approach

You could use a Timer to manually update property values:

qml
Timer {
  interval: 16; repeat: true
  onTriggered: {
    spinner.rotation = (spinner.rotation + 6) % 360
  }
}

This works but reinvents the wheel. You're writing animation logic that QML's animation framework already provides — with better performance and less code.

💡Mental Model

Standalone animations are like ceiling fans: they run continuously at a set speed, independent of anything else. State-driven animations (transitions) are like automatic doors: they only move when triggered by a state change.

The Idea

QML provides animation types that control property values over time:

  • NumberAnimation — animates numeric properties
  • ColorAnimation — animates color properties
  • OpacityAnimator — specialized opacity animation (runs on the rendering thread)
  • RotationAnimator — specialized rotation animation (runs on the rendering thread)
  • SequentialAnimation — runs child animations one after another
  • ParallelAnimation — runs child animations simultaneously
  • PropertyAnimation — base type for any property animation

Let's Build It

qml
import QtQuick
import QtQuick.Window

Window {
  width: 500
  height: 400
  visible: true
  title: "Animations"

  Row {
    anchors.centerIn: parent
    spacing: 40

    // 1. Spinning loader
    Rectangle {
      id: spinner
      width: 60; height: 60; radius: 30
      color: "transparent"
      border.color: "#4ecdc4"; border.width: 4

      Rectangle {
        anchors.top: parent.top; anchors.horizontalCenter: parent.horizontalCenter
        width: 6; height: 16; radius: 3
        color: "#4ecdc4"
      }

      // Continuous rotation
      NumberAnimation on rotation {
        from: 0; to: 360
        duration: 1000
        loops: Animation.Infinite
      }
    }

    // 2. Pulsing dot
    Rectangle {
      id: dot
      width: 40; height: 40; radius: 20
      color: "#ff6b6b"

      Rectangle {
        anchors.fill: parent; radius: parent.radius
        color: "#ff6b6b"; opacity: 0.3

        NumberAnimation on scale {
          from: 0.8; to: 1.8
          duration: 800
          loops: Animation.Infinite
          easing.type: Easing.InOutQuad
        }
        NumberAnimation on opacity {
          from: 0.5; to: 0
          duration: 800
          loops: Animation.Infinite
          easing.type: Easing.InOutQuad
        }
      }
    }

    // 3. Bouncing ball
    Column {
      spacing: 6
      Text { text: "Bounce"; color: "#888"; font.pixelSize: 12; anchors.horizontalCenter: parent.horizontalCenter }

      Rectangle {
        id: ball
        width: 40; height: 40; radius: 20
        color: "#ffe66d"
        y: 0

        SequentialAnimation on y {
          loops: Animation.Infinite

          NumberAnimation {
            to: 80
            duration: 400
            easing.type: Easing.OutQuad
          }
          NumberAnimation {
            to: 0
            duration: 400
            easing.type: Easing.InQuad
          }
          PauseAnimation { duration: 200 }
        }
      }
    }
  }

  // 4. Color-cycling bar
  Rectangle {
    anchors { bottom: parent.bottom; left: parent.left; right: parent.right; bottomMargin: 20; margins: 20 }
    height: 8
    radius: 4

    ColorAnimation on color {
      from: "#ff6b6b"
      to: "#4ecdc4"
      duration: 2000
      loops: Animation.Infinite
    }
  }

  // 5. Property animation via Behavior
  Rectangle {
    id: animatedBox
    x: 20; y: 20
    width: 100; height: 60; radius: 8
    color: "#45b7d1"

    Behavior on x {
      NumberAnimation { duration: 300; easing.type: Easing.OutBounce }
    }
    Behavior on y {
      NumberAnimation { duration: 300; easing.type: Easing.OutBounce }
    }

    Text { text: "Click"; anchors.centerIn: parent; color: "white"; font.bold: true }

    MouseArea {
      anchors.fill: parent
      onClicked: {
        animatedBox.x = Math.random() * (parent.width - 100)
        animatedBox.y = Math.random() * (parent.height - 80)
      }
    }
  }
}
🛠️Let's Build It

Animation patterns:

  • NumberAnimation on rotation { loops: Animation.Infinite } — infinite looping animation. from and to define the range.
  • SequentialAnimation — runs child animations in order. Here, bouncing down with OutQuad easing, then up with InQuad easing, then pause.
  • Behavior on x { NumberAnimation { ... } } — whenever x changes, animate to the new value. The animation plays for any change, not just state changes.
  • ColorAnimation — cycles the bar's color between red and teal continuously.

Easing Curves

The easing type dramatically changes how an animation feels:

EasingFeel
LinearMechanical, constant speed
InQuad / OutQuadGentle acceleration/deceleration
InOutQuadSmooth start and end (most natural)
OutBounceBounces at the end (playful)
OutElasticOvershoots and snaps back
InBackPulls back before moving forward

Common Mistake

Animating on the UI thread causing jank. OpacityAnimator and RotationAnimator run on the scene graph's render thread, not the UI thread. Use them for performance-critical animations. NumberAnimation runs on the UI thread and can jank if the UI thread is busy.

Forgetting alwaysRunToEnd. If you stop an animation (by setting running: false), it jumps to the final value immediately. Set alwaysRunToEnd: true on the animation to let it finish gracefully.

Exercises

Exercise
Create a pulsing heart shape (using unicode ❤️) that scales from 1.0 to 1.3 and back every 500ms using SequentialAnimation.
⭐⭐Exercise
Build a progress bar that fills from 0% to 100% over 3 seconds when a button is clicked. Use NumberAnimation on the fill rectangle's width.
⭐⭐⭐Exercise
Create a particle effect using a Repeater of 20 small rectangles, each with its own SequentialAnimation, spawning at staggered intervals using PauseAnimation.
What You've Learned
  • Standalone animations run continuously or on-demand via NumberAnimation, ColorAnimation, etc.
  • Use SequentialAnimation and ParallelAnimation to compose complex animations
  • Behavior animates property changes wherever they originate
  • Animator types (OpacityAnimator, RotationAnimator) run on the render thread for better performance

Built with VitePress. Licensed under MIT.