TransitionToggleRepeat is a utility class that repeatedly toggles a USS class on a VisualElement, creating looping CSS transition animations. It's useful for breathing effects, pulsing buttons, or any repeating visual state change.
Important: The USS class you toggle must define a
transitionon at least one property. Otherwise, theTransitionEndEventwill not fire and the animation loop won't continue.
First, create USS styles with transitions:
.my-element {
opacity: 1;
transition: opacity 0.5s ease-in-out;
}
.my-element.fade-anim {
opacity: 0.5;
}using UnityEngine.UIElements;
using CupkekGames.Luna;
public class PulsingButton : MonoBehaviour
{
private TransitionToggleRepeat _pulseAnimation;
private void Start()
{
var button = _uiDocument.rootVisualElement.Q<Button>("PulseButton");
// Create repeating animation that toggles "fade-anim" class
// with 200ms delay between transitions
_pulseAnimation = new TransitionToggleRepeat(
button,
"fade-anim",
delayMsInterval: 200
);
}
private void OnEnable()
{
// Start animation with 100ms initial delay
_pulseAnimation.Start(delayMs: 100);
}
private void OnDisable()
{
_pulseAnimation.Pause();
}
}public TransitionToggleRepeat(
VisualElement ve,
string ussClassName,
int delayMsInterval
)| Parameter | Type | Description |
|---|---|---|
| ve | VisualElement | The element to animate |
| ussClassName | string | USS class to toggle |
| delayMsInterval | int | Delay in milliseconds between each toggle |
Starts the repeating animation.
public void Start(long delayMs)| Parameter | Type | Description |
|---|---|---|
| delayMs | long | Initial delay before first toggle |
Stops the animation and unregisters the transition event callback.
public void Pause()Start() is called, it schedules the first class toggle after delayMsTransitionEndEventdelayMsInterval millisecondsThis creates a seamless loop where the element transitions between two states defined by the presence or absence of the USS class.
.character-portrait {
scale: 1;
transition: scale 1s ease-in-out;
}
.character-portrait.breathing {
scale: 1.05;
}var portrait = root.Q<VisualElement>("Portrait");
var breathing = new TransitionToggleRepeat(portrait, "breathing", 100);
breathing.Start(0);.notification-icon {
--glow-opacity: 0;
transition: --glow-opacity 0.5s ease-in-out;
}
.notification-icon.glow-pulse {
--glow-opacity: 1;
}var icon = root.Q<VisualElement>("NotificationIcon");
var pulse = new TransitionToggleRepeat(icon, "glow-pulse", 300);
pulse.Start(0);.continue-arrow {
translate: 0 0;
transition: translate 0.3s ease-in-out;
}
.continue-arrow.bounce {
translate: 0 -10px;
}var arrow = root.Q<VisualElement>("ContinueArrow");
var bounce = new TransitionToggleRepeat(arrow, "bounce", 100);
bounce.Start(500); // Start after 500msA complete example that manages multiple rotating avatar animations:
using System.Collections.Generic;
using System.Linq;
using CupkekGames.Luna;
using UnityEngine;
using UnityEngine.UIElements;
public class AnimatedAvatars
{
private readonly List<Sprite> _avatars;
private readonly List<VisualElement> _avatarElements;
private readonly List<TransitionToggleRepeat> _transitions = new();
public AnimatedAvatars(List<Sprite> avatars, List<VisualElement> avatarElements)
{
_avatars = avatars;
_avatarElements = avatarElements;
foreach (VisualElement avatar in _avatarElements)
{
_transitions.Add(new TransitionToggleRepeat(avatar, "avatar_anim_rotate", 200));
}
}
public void StartAnimation()
{
RandomizeAvatars();
foreach (TransitionToggleRepeat transition in _transitions)
{
transition.Start(0);
}
}
public void StopAnimation()
{
foreach (TransitionToggleRepeat transition in _transitions)
{
transition.Pause();
}
}
private void RandomizeAvatars()
{
List<int> shuffledIndices = Enumerable.Range(0, _avatars.Count)
.OrderBy(_ => Random.value).ToList();
for (int i = 0; i < _avatarElements.Count; i++)
{
if (i >= shuffledIndices.Count)
{
_avatarElements[i].parent.style.display = DisplayStyle.None;
continue;
}
_avatarElements[i].style.backgroundImage =
new StyleBackground(_avatars[shuffledIndices[i]]);
}
}
}The Visual Novel controller uses TransitionToggleRepeat for avatar breathing animations and next icon indicators:
// Animate avatar with breathing effect
_avatarLeftSchedule = new TransitionToggleRepeat(_avatarLeft, "vn_avatar_anim", 200);
_avatarRightSchedule = new TransitionToggleRepeat(_avatarRight, "vn_avatar_anim", 200);
// Animate "next" icon
_nextIconSchedule = new TransitionToggleRepeat(_nextIcon, "vn_next_icon_anim", 100);
// Show avatar with animation
public void ShowAvatarLeft(Sprite sprite)
{
_avatarLeft.style.backgroundImage = new StyleBackground(sprite);
_avatarLeftContainer.style.display = DisplayStyle.Flex;
_avatarLeftSchedule.Start(1);
}
// Hide avatar and stop animation
public void HideAvatarLeft()
{
_avatarLeftSchedule.Pause();
_avatarLeftContainer.style.display = DisplayStyle.None;
}transition duration + delayMsIntervalPause() before the VisualElement is removed from the hierarchyTransitionEndEvent, but the class handles this with a flag to prevent multiple togglesSettings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction