# Counter Widget

> Animated number counter — odometer-style digit flip from start to end. Triggered when widget scrolls into viewport via Waypoints.js.

**Class file:** `includes/Elements/Counter.php` (1,363 lines)
**Slug:** `counter` (widget id `eael-counter`)
**Public docs:** <https://essential-addons.com/elementor/docs/counter/>
**Pro-shared:** Pro-only. Vendor: Odometer + Waypoints (both Pro-bundled).

## Overview

Animated number counter — typical "100,000+ customers" stat display. Number animates from a start value to an end value when the widget enters the viewport. Uses Odometer.js for the digit-flip visual effect; Waypoints.js detects scroll-into-viewport. Optional prefix / suffix text around the number, icon decoration, multiple themes.

## Pro vs Lite

Pro-only.

## File Map

| File | Role |
| --- | --- |
| `includes/Elements/Counter.php` | Widget class (1,363 lines) |
| `src/css/view/counter.scss` → `assets/front-end/css/view/counter.min.css` | Pro styling |
| `src/js/view/counter.js` → `assets/front-end/js/view/counter.min.js` | Odometer + Waypoints init |
| `assets/front-end/css/lib-view/odometer/odometer-theme-default.min.css` | Odometer theme (`lib`, view) |
| `assets/front-end/js/lib-view/waypoint/waypoints.min.js` | Waypoints (`lib`) |
| (Odometer JS path) | Odometer library |
| `config.php` entry `'counter'` | lib CSS + Pro CSS + lib JS + Pro JS |

## Architecture

- **Composes Lite's `Helper`** (line 11).
- **Odometer + Waypoints stack** — Waypoints detects when the widget enters viewport; Odometer animates the digit flip.
- **No JS trigger override** — animation runs every time widget enters viewport (e.g. if user scrolls up then down, fires again — verify).
- **Configurable end-number, animation duration, format** (currency / percent / number).
- **Multiple themes** — Odometer has built-in themes (default, minimal, train-station, car). Pro likely exposes a SELECT.

## Render Output

```html
<div class="eael-counter-wrap">
  [?] <i class="eael-counter-icon {icon-class}"></i>
  <div class="eael-counter-content">
    <h2 class="eael-counter-title">{title}</h2>
    <div class="eael-counter-number-wrap">
      [?] <span class="prefix">{prefix}</span>
      <span class="odometer" data-target="{end_number}">0</span>
      [?] <span class="suffix">{suffix}</span>
    </div>
    [?] <p class="eael-counter-description">{description}</p>
  </div>
</div>
```

## Controls Reference

| Control id | Tab → Section | Type | Purpose |
| --- | --- | --- | --- |
| Target number | Content → Counter | NUMBER | End value |
| Prefix / suffix | Content → Counter | TEXT | Surrounding text (`$`, `+`, `K`, `%`) |
| Animation duration | Content → Counter | NUMBER | ms |
| Odometer theme | Content → Counter | SELECT | Default / minimal / train-station / car |
| Title + description | Content → Text | TEXT / TEXTAREA | Additional content |
| Icon | Content → Icon | ICONS | Optional icon decoration |
| Per-region styling | Style → ... | various | Number / title / description / icon styling |

## Conditional Dependencies

```text
icon_show = 'yes' → ICONS picker + style controls
prefix or suffix set → render visible
```

## JavaScript Lifecycle

```js
var CounterHandler = function( $scope, $ ) {
    var $counter = $scope.find( '.odometer' );
    new Waypoint( {
        element: $counter[0],
        handler: function() {
            new Odometer( {
                el: $counter[0],
                value: 0,
                duration: $counter.data( 'duration' ),
                theme: $counter.data( 'theme' )
            } ).update( $counter.data( 'target' ) );
        },
        offset: '80%'
    } );
};
```

## Hooks & Filters

Standard widget render.

## Common Issues

| Symptom | Cause | Fix |
| --- | --- | --- |
| Animation runs in editor | `isEditMode` not gated | Add `if (window.isEditMode) return;` |
| Number jumps to end without animation | Odometer init before DOM paint | Wrap in `setTimeout` or use Waypoint properly |
| Triggers every scroll into viewport | Waypoint not destroyed after first fire | Set `triggerOnce: true` in Waypoint config |
| Comma-separated digits wrong | Locale not set | Configure Odometer `format` option per locale |
| Decimal numbers not animating | Odometer integer-only by default | Use Odometer `digits` config or render manually |

## Known Limitations

- **Waypoints unmaintained upstream** — IntersectionObserver is the modern replacement
- **No `prefers-reduced-motion`** — animation always runs
- **Integer-only by default** — decimal targets need extra config
- **Locale-aware formatting** depends on Odometer's `format` setting; not auto-detected
- **Re-trigger on scroll** — depending on `triggerOnce` config, may animate multiple times per session

## Cross-References

- Sibling: [`stacked-cards.md`](stacked-cards.md), [`advanced-search.md`](advanced-search.md)
- Shared patterns: [`_patterns.md`](_patterns.md)
