| |

Angular 67 🅰️ Advanced Animations and Route Transitions

In the previous chapter, you learned the foundational building blocks of Angular animations: triggers, states, transitions, and the enter/leave lifecycle. Those primitives handle single elements with simple state changes. Real applications, however, need far more: cascading lists that animate in sequence, parent-child animations that coordinate without blocking, and seamless transitions between routed views. This chapter covers the advanced techniques that make those scenarios possible.

One important context: the @angular/animations package is deprecated as of v20.2, with removal planned for v23 . The Angular team recommends native CSS animations for all new code. But the architectural patterns — querying children, staggering delays, grouping parallel steps, coordinating parent-child triggers — remain relevant, and you will encounter them constantly in existing applications. Understanding them is essential whether you maintain legacy code or simply want to think clearly about motion design in Angular.

Key point: Advanced animations build on the primitives from the previous chapter. The query() function finds inner elements and applies animation steps to them. The stagger() function cascades delays between queried elements. The group() function runs multiple animation steps in parallel, while sequence() runs them one after another. The animateChild() function releases child animations that would otherwise be blocked by a parent’s animation .


Why advanced animations exist

Simple animations handle single elements with two or three states. But real UI components rarely exist in isolation.

The collection problem. A list of items entering the view one by one creates a cascade effect that feels natural and guides the eye. Without stagger(), all items animate simultaneously, losing the effect entirely. A dashboard with twelve widgets entering at once feels mechanical; the same widgets entering with a 30-millisecond delay between them feels intentional .

The coordination problem. When a modal opens, you might want the backdrop to fade in while the dialog scales up — two different motions with different easings running at the same time. The group() function lets you run animation steps in parallel, so the backdrop can use ease-out while the dialog uses cubic-bezier() .

The parent-child problem. Angular’s rule is strict: parent animations always take priority, and child animations are blocked unless explicitly queried . A component with its own :enter trigger will not animate if its parent is also animating. The animateChild() function is the release valve — it tells the parent to let the child run .

The route transition problem. Navigating between views is one of the most visible moments in an application. The legacy approach wraps router-outlet with a trigger and queries :enter and :leave to animate the incoming and outgoing components . The newer approach uses the browser’s View Transitions API, which Angular Router integrates with through withViewTransitions() .

The trade-off. These functions add complexity. A simple fade needs only transition(':enter', [animate('300ms', ...)]). A staggered list with parallel animations and child coordination needs nested query(), stagger(), group(), and animateChild() calls. The complexity is real, but so is the visual quality it produces.


a. Querying and Staggering: Animating Collections

When you animate a list or grid, you rarely want every element to move in perfect unison. A staggered entrance — where each item starts slightly after the previous one — creates a cascade effect that feels natural and guides the eye. Angular’s query() and stagger() functions make this straightforward.

The query() function finds inner HTML elements and applies animation steps to them. Its first argument is a CSS selector string, which can also include Angular-specific tokens like :enter, :leave, :animating, and @triggerName . The stagger() function then applies a delay between each queried element’s animation.

import { trigger, transition, query, style, stagger, animate } from '@angular/animations';

animations: [
  trigger('listAnimation', [
    transition(':enter', [
      query('.list-item', [
        style({ opacity: 0, transform: 'translateY(-100px)' }),
        stagger(30, [
          animate('500ms cubic-bezier(0.35, 0, 0.25, 1)', 
            style({ opacity: 1, transform: 'none' }))
        ]),
      ]),
    ]),
  ]),
]

Here, query('.list-item') selects all elements with that class inside the trigger’s host element. Each item starts transparent and shifted up by 100 pixels. The stagger(30, ...) wrapper delays each item’s animation by 30 milliseconds, so they cascade from top to bottom. The cubic-bezier easing creates a smooth, decelerating entrance . Without stagger(), all items would animate simultaneously, losing the cascade effect entirely.

The query tokens are worth memorizing because they unlock powerful patterns. :enter targets elements entering the DOM, :leave targets elements leaving, :animating targets currently animating elements, and @triggerName targets elements with a specific trigger . The :self token refers to the animating element itself. These tokens let you write queries that respond precisely to lifecycle events rather than relying solely on CSS class selectors.

There is an important caveat about what counts as “entering” and “leaving.” Not all child elements are considered as entering/leaving in the way you might expect. The query() function’s :enter and :leave tokens match elements that are being added to or removed from the DOM as part of the current animation phase, not every element that happens to be in the DOM during an animation . This can be counterintuitive, and it is worth consulting the query API documentation when behavior surprises you.

The stagger() function accepts two arguments: the delay in milliseconds and the animation steps to apply to each element. The delay is applied between each successive element, not to the entire set. So stagger(30, ...) means item one starts at 0ms, item two at 30ms, item three at 60ms, and so on. For longer lists, the total animation time grows linearly with the item count. If you have fifty items and a 30ms stagger, the last item starts 1,470ms after the first. Consider capping the stagger for very long lists or using a smaller delay to keep the total duration reasonable.


b. Parallel and Sequential Steps: group, sequence, animateChild

Sometimes you need multiple animations to run simultaneously on the same element — fading in while sliding, for example — or you need child components with their own animations to run in coordination with the parent. Angular provides three functions for these scenarios.

The group() function runs multiple animation steps in parallel. It is particularly useful when you want different easing curves for different CSS properties on the same element . The sequence() function runs steps one after another, waiting for each to complete. And animateChild() lets a parent animation query child elements that have their own triggers and run them in the same timeframe.

trigger('parentChild', [
  transition(':enter', [
    group([
      query('@childAnimation', [
        animateChild(),
      ]),
      query('.content', [
        style({ opacity: 0 }),
        animate('300ms', style({ opacity: 1 })),
      ]),
    ]),
  ]),
])

This trigger does two things in parallel: it queries child elements with the @childAnimation trigger and runs them via animateChild(), while simultaneously fading in .content elements. Without animateChild(), child animations would be blocked by the parent’s animation. Angular’s rule is strict: “the parent animation always gets priority and child animations are blocked” unless explicitly queried . The group() wrapper ensures both sets of animations run at the same time rather than sequentially.

A crucial distinction: group() groups animation steps, not animated elements . This means you use it to coordinate multiple motions on the same subject, or multiple queries that should fire together. The sequence() function, by contrast, waits for each step to finish before starting the next. A sequence() is useful for multi-stage reveals where timing matters — for example, a card that first slides up, then fades in its content, then pulses a border.

There is also a special binding for disabling animations: @.disabled. When placed on an element with a truthy value, it prevents all animations on that element and any nested elements . This is useful during end-to-end testing to eliminate timing jitter. The binding does not stop animation callbacks from firing, however, so if you use @.disabled with animation callbacks, ensure those callbacks do not trigger further state changes that could create loops.

A subtle but important behavior: a child animation can still run on a disabled parent in two ways. First, a parent animation can use query() to collect inner elements located in disabled areas — those elements can still animate. Second, a child animation can be queried by a parent and then animated with animateChild() . This means @.disabled is not a hard barrier for all nested animations; it is a default that can be overridden through explicit queries.


c. Route Transitions: Two Approaches

Animating between routed views is one of the most visible improvements you can make to an application. Angular supports two fundamentally different approaches: the legacy animations package (binding a trigger to the router outlet) and the newer native View Transitions API (using the browser’s built-in document.startViewTransition).

The legacy approach wraps router-outlet with an animation trigger. When navigation occurs, the entering component is treated as :enter and the leaving component as :leave . You define a trigger with transition('* => *') to catch any route change, then query the entering and leaving views and animate them.

// route-transition.ts
import { trigger, transition, query, style, animate, group } from '@angular/animations';

export const routeTransition = trigger('routeTransition', [
  transition('* => *', [
    query(':enter, :leave', [
      style({ position: 'absolute', top: 0, left: 0, right: 0, width: '100%' }),
    ], { optional: true }),
    query(':enter', [
      style({ opacity: 0, transform: 'translateY(20px)' }),
    ], { optional: true }),
    group([
      query(':leave', [
        animate('300ms ease-out', style({ opacity: 0, transform: 'translateY(-20px)' })),
      ], { optional: true }),
      query(':enter', [
        animate('300ms 100ms ease-in', style({ opacity: 1, transform: 'translateY(0)' })),
      ], { optional: true }),
    ]),
  ]),
]);

The { optional: true } option prevents errors when no leaving element exists — which happens on the initial route load . The group() ensures the leaving and entering animations run in parallel, with the entering animation delayed by 100ms so the outgoing view starts moving first. You then bind this trigger to the outlet: <div [@routeTransition]="outlet.isActivated ? outlet.activatedRoute : ''"><router-outlet #outlet="outlet"></router-outlet></div>.

The newer approach uses Angular Router’s built-in support for the browser’s View Transitions API. Add withViewTransitions() to your router configuration, and Angular automatically wraps route changes in document.startViewTransition() .

// app.config.ts
import { provideRouter, withViewTransitions } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withViewTransitions()),
  ],
};

This approach is a progressive enhancement: browsers that don’t support the View Transitions API simply perform normal DOM updates without animation . You customize transitions with CSS using ::view-transition-old() and ::view-transition-new() pseudo-elements, which must be defined in global styles, not component styles, because Angular’s view encapsulation prevents component styles from targeting those pseudo-elements . The onViewTransitionCreated callback gives you advanced control — you can inspect the from and to route snapshots and call transition.skipTransition() to skip animations for specific navigation scenarios, such as layout switches or initial loads . A common use is checking prefers-reduced-motion: reduce and skipping the transition for users who have requested reduced motion .

The shared-element trick is one of the most powerful features of native View Transitions. Give an element on the outgoing page and an element on the incoming page the same view-transition-name, and the browser animates one into the other — position, size, the lot . This is the effect native apps have had for years, and it takes two lines of CSS. One rule to respect: a view-transition-name must be unique on the page at transition time. For a list of cards, that means assigning the name dynamically to the clicked card only, or you will get a console warning and no animation .


Complete Example Session

This session builds a staggered list animation, a parent-child coordination, and a route transition, demonstrating the full workflow.

// ============================================
// PART 1: THE STAGGERED LIST
// ============================================

import { trigger, transition, query, style, stagger, animate } from '@angular/animations';

@Component({
  selector: 'app-hero-list',
  template: `
    <ul [@listAnimation]="heroes.length">
      <li *ngFor="let hero of heroes" class="hero-item">{{ hero.name }}</li>
    </ul>
  `,
  animations: [
    trigger('listAnimation', [
      transition(':enter', [
        query('.hero-item', [
          style({ opacity: 0, transform: 'translateY(-50px)' }),
          stagger(50, [
            animate('400ms ease-out', style({ opacity: 1, transform: 'none' })),
          ]),
        ]),
      ]),
    ]),
  ],
})
export class HeroListComponent {
  heroes = [{ name: 'Spider-Man' }, { name: 'Iron Man' }, { name: 'Thor' }];
}

// ============================================
// PART 2: THE PARALLEL ANIMATION WITH GROUP
// ============================================

trigger('flyInOut', [
  transition(':enter', [
    style({ width: 10, transform: 'translateX(50px)', opacity: 0 }),
    group([
      animate('0.3s 0.1s ease', style({ transform: 'translateX(0)', width: '*' })),
      animate('0.3s ease', style({ opacity: 1 })),
    ]),
  ]),
])

// ============================================
// PART 3: THE PARENT-CHILD COORDINATION
// ============================================

// child component
@Component({
  selector: 'app-child-box',
  template: `<div [@childBox]="'in'">Child</div>`,
  animations: [
    trigger('childBox', [
      transition(':enter', [
        style({ opacity: 0, scale: 0.5 }),
        animate('200ms', style({ opacity: 1, scale: 1 })),
      ]),
    ]),
  ],
})
export class ChildBoxComponent {}

// parent component
@Component({
  selector: 'app-parent',
  template: `<app-child-box [@parentTrigger]></app-child-box>`,
  animations: [
    trigger('parentTrigger', [
      transition(':enter', [
        query('@childBox', [animateChild()]),
      ]),
    ]),
  ],
})
export class ParentComponent {}

// ============================================
// PART 4: THE SEQUENCE ANIMATION
// ============================================

trigger('multiStage', [
  transition(':enter', [
    sequence([
      style({ opacity: 0 }),
      animate('200ms', style({ opacity: 1 })),
      animate('300ms', style({ transform: 'translateX(100px)' })),
    ]),
  ]),
])

// ============================================
// PART 5: THE ROUTE TRANSITION (LEGACY)
// ============================================

// route-transition.ts
export const routeTransition = trigger('routeTransition', [
  transition('* => *', [
    query(':enter, :leave', [
      style({ position: 'absolute', top: 0, left: 0, right: 0, width: '100%' }),
    ], { optional: true }),
    query(':enter', [
      style({ opacity: 0, transform: 'translateY(20px)' }),
    ], { optional: true }),
    group([
      query(':leave', [
        animate('300ms ease-out', style({ opacity: 0, transform: 'translateY(-20px)' })),
      ], { optional: true }),
      query(':enter', [
        animate('300ms 100ms ease-in', style({ opacity: 1, transform: 'translateY(0)' })),
      ], { optional: true }),
    ]),
  ]),
]);

// app.component.ts
@Component({
  template: `
    <div [@routeTransition]="outlet.isActivated ? outlet.activatedRoute : ''">
      <router-outlet #outlet="outlet"></router-outlet>
    </div>
  `,
  animations: [routeTransition],
})
export class AppComponent {}

// ============================================
// PART 6: THE ROUTE TRANSITION (NATIVE)
// ============================================

// app.config.ts
import { provideRouter, withViewTransitions } from '@angular/router';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes, withViewTransitions({
      onViewTransitionCreated: ({ transition, from, to }) => {
        if (!from) transition.skipTransition();
      },
    })),
  ],
});

// ============================================
// PART 7: THE VIEW TRANSITION CSS
// ============================================

/* global-styles.css */
@keyframes slide-in {
  from { opacity: 0; transform: translateX(50px); }
  to { opacity: 1; transform: translateX(0); }
}

@keyframes slide-out {
  from { opacity: 1; transform: translateX(0); }
  to { opacity: 0; transform: translateX(-50px); }
}

::view-transition-new(root) {
  animation: slide-in 300ms ease-out;
}

::view-transition-old(root) {
  animation: slide-out 300ms ease-out;
}

// ============================================
// PART 8: THE SHARED-ELEMENT MORPH
// ============================================

/* list page */
.product-card__image { view-transition-name: product-hero; }

/* detail page */
.product-detail__hero { view-transition-name: product-hero; }

// ============================================
// PART 9: THE REDUCED-MOTION GUARD
// ============================================

withViewTransitions({
  onViewTransitionCreated: ({ transition }) => {
    const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduced) transition.skipTransition();
  },
})

// ============================================
// PART 10: THE QUERY TOKEN TRAPS
// ============================================

// ❌ Not all child elements are :enter/:leave
query('.item', [ /* only matches elements entering/leaving */ ])

// ✅ Use optional for route transitions
query(':enter', [ /* ... */ ], { optional: true })

// ❌ Wildcard before specific
transition('* => *', [animate('1s')]),
transition('open => closed', [animate('0.2s')]),  // never runs

// ✅ Specific before wildcard
transition('open => closed', [animate('0.2s')]),
transition('* => *', [animate('1s')]),

The ten parts cover the staggered list, the parallel animation, the parent-child coordination, the sequence animation, the legacy route transition, the native route transition, the view transition CSS, the shared-element morph, the reduced-motion guard, and the query token traps.


Quick Reference

The Complex Animation Functions

FunctionPurposeKey Detail
query()Find inner elementsSupports Angular tokens: :enter, :leave, :animating, @triggerName, :self
stagger()Cascade delayFirst arg is milliseconds between each element
group()Parallel stepsRuns inner steps simultaneously
sequence()Sequential stepsRuns inner steps one after another
animateChild()Run child animationsRequired or child triggers are blocked

The Query Tokens

TokenTargets
:enterElements entering the DOM
:leaveElements leaving the DOM
:animatingElements currently animating
@triggerNameElements with a specific trigger
:selfThe animating element itself

The Route Transition Approaches

ApproachConfigurationBrowser SupportCustomization
Legacy trigger on outlet[@routeTransition] on wrapperAll browsersFull Angular animation API
Native View TransitionswithViewTransitions()Limited (progressive enhancement)CSS pseudo-elements + callbacks

The View Transition Callbacks

CallbackPurpose
onViewTransitionCreatedInspect from/to routes, skip transitions
transition.skipTransition()Abort animation, keep navigation

Best Practices

✅ Do This:

// Use optional for route transition queries
query(':enter', [ /* ... */ ], { optional: true })           // ✅
// Use animateChild for child triggers
query('@childTrigger', [animateChild()])                     // ✅
// Place view transition CSS in global styles
/* global-styles.css */                                      // ✅
// Skip initial transition with skipTransition
if (!from) transition.skipTransition();                      // ✅
// Respect prefers-reduced-motion
if (reduced) transition.skipTransition();                    // ✅

❌ Don’t Do This:

// Don't forget optional on route queries
query(':enter', [ /* ... */ ])                               // ❌ crashes on load
// Don't expect child animations to run automatically
// Without animateChild(), they are blocked.                // ❌
// Don't put view transition CSS in component styles
// Angular's encapsulation blocks the pseudo-elements.      // ❌
// Don't animate the first page load
// Skip it with skipTransition().                           // ❌
// Don't mix router-outlet animations with view transitions
// They fight over the same moment.                         // ❌

Common Pitfalls

PitfallWhy It HappensFix
Child animations never runParent blocks themUse query('@childTrigger', animateChild())
Stagger has no effectstagger() wraps style() not animate()Ensure stagger() wraps animate()
Route transition crashes on loadNo leaving element existsAdd { optional: true } to queries
View transition CSS ignoredDefined in component stylesMove to global styles file
List animation doesn’t re-triggerBound value unchangedBind to length or counter, not static value
Wildcard overrides specificOrder of transitions wrongPlace specific transitions before wildcards
Native and legacy fightBoth own the same momentUse one approach per project

Real-World Examples

1. Hero List Entrance

query('.hero', [
  style({ opacity: 0, transform: 'translateY(-100px)' }),
  stagger(30, [
    animate('500ms cubic-bezier(0.35, 0, 0.25, 1)', style({ opacity: 1, transform: 'none' })),
  ]),
])

2. Parallel Fade and Slide

group([
  animate('0.3s 0.1s ease', style({ transform: 'translateX(0)', width: '*' })),
  animate('0.3s ease', style({ opacity: 1 })),
])

3. Parent-Child Release

query('@childAnimation', [animateChild()])

4. Multi-Stage Sequence

sequence([
  animate('200ms', style({ opacity: 1 })),
  animate('300ms', style({ transform: 'translateX(100px)' })),
])

5. Legacy Route Crossfade

group([
  query(':leave', [animate('300ms', style({ opacity: 0 }))], { optional: true }),
  query(':enter', [animate('300ms 100ms', style({ opacity: 1 }))], { optional: true }),
])

6. Native View Transition Bootstrap

provideRouter(routes, withViewTransitions())

7. View Transition CSS

::view-transition-new(root) { animation: slide-in 300ms ease-out; }
::view-transition-old(root) { animation: slide-out 300ms ease-out; }

8. Shared-Element Morph

.product-card__image { view-transition-name: product-hero; }
.product-detail__hero { view-transition-name: product-hero; }

9. Reduced-Motion Guard

if (matchMedia('(prefers-reduced-motion: reduce)').matches) transition.skipTransition();

10. Skip Initial Load

onViewTransitionCreated: ({ from }) => { if (!from) transition.skipTransition(); }

Visual: Complex Sequence Flow

┌──────────────────────────────────────────────┐
│  COMPLEX SEQUENCE FLOW                       │
│                                              │
│  query('.item') ──> stagger(30) ──> animate  │
│       │                  │                │  │
│       │                  │                │  │
│       │                  │                └─ Each item
│       │                  └─ 30ms delay       animates
│       └─ Finds all .item elements            │
│                                              │
│  Result: Item 1 at 0ms, Item 2 at 30ms,      │
│          Item 3 at 60ms, ...                 │
│                                              │
└──────────────────────────────────────────────┘

Visual: Parent-Child Animation Blocking

┌──────────────────────────────────────────────┐
│  PARENT-CHILD BLOCKING                       │
│                                              │
│  WITHOUT animateChild():                     │
│  ┌─────────────┐                             │
│  │   Parent    │ ──blocks──>  ┌───────────┐  │
│  │  Animation  │              │   Child   │  │
│  └─────────────┘              │ Animation │  │
│                               │ (never    │  │
│                               │  runs)    │  │
│                               └───────────┘  │
│                                              │
│  WITH animateChild():                        │
│  ┌─────────────┐                             │
│  │   Parent    │                             │
│  │  Animation  │                             │
│  └──────┬──────┘                             │
│         │ query('@child')                    │
│         ▼                                    │
│  ┌─────────────┐                             │
│  │    Child    │ ──> runs                    │
│  │  Animation  │                             │
│  └─────────────┘                             │
│                                              │
└──────────────────────────────────────────────┘

Visual: View Transition Lifecycle

┌──────────────────────────────────────────────┐
│  VIEW TRANSITION LIFECYCLE                   │
│                                              │
│  1. Capture old state (screenshot)           │
│         │                                    │
│         ▼                                    │
│  2. Execute DOM update (route activation)    │
│         │                                    │
│         ▼                                    │
│  3. Capture new state (screenshot)           │
│         │                                    │
│         ▼                                    │
│  4. Play transition (CSS animations)         │
│                                              │
│  Progressive enhancement: browsers           │
│  without support skip steps 1, 3, and 4.     │
│                                              │
└──────────────────────────────────────────────┘

Visual: Transition Matching Order

┌──────────────────────────────────────────────┐
│  TRANSITION MATCHING ORDER                   │
│                                              │
│  transitions: [                              │
│    1. 'open => closed'    ← matched first    │
│    2. '* => *'            ← fallback         │
│  ]                                           │
│                                              │
│  ❌ WRONG ORDER:                             │
│  transitions: [                              │
│    1. '* => *'            ← catches all      │
│    2. 'open => closed'    ← never reached    │
│  ]                                           │
│                                              │
│  Specific first, wildcard last.              │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Complex functionsquery, stagger, group, sequence, animateChild
Query tokens:enter, :leave, :animating, @triggerName, :self
Parent-child ruleParent blocks child unless animateChild()
Legacy route transitionTrigger on outlet, :enter/:leave queries
Native route transitionwithViewTransitions(), CSS pseudo-elements
View transition CSSMust be in global styles
Initial load skipif (!from) transition.skipTransition()
Reduced motionmatchMedia('(prefers-reduced-motion: reduce)')
Animations packageDeprecated v20.2, removal planned v23

Key takeaways:

  • query() finds inner elements and applies animation steps to them. It supports Angular-specific tokens like :enter, :leave, :animating, and @triggerName that respond to lifecycle events rather than CSS classes alone .
  • stagger() cascades delays between queried elements. The first argument is milliseconds between each item. Item one starts at 0ms, item two at 30ms, item three at 60ms. For long lists, cap the total duration .
  • group() runs steps in parallel, sequence() runs them in order. Use group() when different CSS properties need different easings. Use sequence() when stages must complete before the next begins .
  • Parent animations block child animations. Unless you query with animateChild(), child triggers wait for the parent to finish. This is Angular’s most important coordination rule .
  • Legacy route transitions wrap router-outlet with a trigger. Query :enter and :leave, use group() for parallel motion, and always add { optional: true } to avoid crashes on initial load .
  • Native View Transitions are simpler to configure. withViewTransitions() enables browser-level page transitions. Customize with CSS pseudo-elements in global styles, and use onViewTransitionCreated for advanced control .
  • Shared-element morphs are the native killer feature. Give elements on outgoing and incoming pages the same view-transition-name, and the browser animates position and size automatically. Ensure the name is unique at transition time .
  • Skip transitions when appropriate. The initial load has no outgoing page. Users with prefers-reduced-motion: reduce should not see full-page motion. Use skipTransition() for both .
  • Don’t mix legacy and native approaches. Router-outlet animations and View Transitions both want to own the same moment. They fight. Choose one per project .

Remember: Advanced Angular animations turn collections into cascades, coordinate parent-child triggers, and make route changes seamless. The query() and stagger() functions handle lists. group() and sequence() control parallelism and order. animateChild() releases blocked children. For route transitions, the legacy trigger-on-outlet approach works everywhere and offers full control, while the native View Transitions API is simpler, faster, and progressive by default. Since the animations package is deprecated, prefer native for new projects — but understand the legacy patterns, because you will encounter them in existing code.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!