# Content Toggle Widget

> Two-state switcher that toggles between primary and secondary content blocks. Common use: monthly/annually pricing tables, light/dark image variants, content-type comparisons.

**Class file:** `includes/Elements/Toggle.php` (1,805 lines)
**Slug:** `toggle` (widget id `eael-toggle`)
**Public docs:** <https://essential-addons.com/elementor/docs/content-toggle/>
**Pro-shared:** Pro-only. Uses Lite's `Classes\Helper`.

## Overview

Renders a switch / pill / button-style toggle with two states (primary + secondary). Each state can be one of three content types:

- **Saved Elementor template** — pick a Templately / Elementor template by ID
- **Custom content** — WYSIWYG block
- **Image** — single image

User clicks the toggle, the matching content swaps. Often used for pricing-table monthly/annual rate toggles, but generic enough for any two-state comparison.

## Pro vs Lite

Pro-only.

## File Map

| File | Role |
| --- | --- |
| `includes/Elements/Toggle.php` | Widget class (1,805 lines) |
| `src/css/view/toggle.scss` → `assets/front-end/css/view/toggle.min.css` | Switch styling |
| `src/js/view/toggle.js` → `assets/front-end/js/view/toggle.min.js` | State swap on click |
| `config.php` entry `'toggle'` | Self CSS + self JS |

## Architecture

- **Content-type per state** — line 191: `primary_content_type` SELECT with options `template` / `content` / `image`. Line 198: when `'template'` selected, a `primary_templates` control (line 205) picks an Elementor saved template.
- **Sanitization at render** — line 89-91: `primary_content_type` and `secondary_content_type` sanitized via `sanitize_text_field`. Defensive defaulting to empty when null.
- **`is_dynamic_content` flag** (line 91) — `true` when either side uses a template (dynamic content can change without page-edit). Drives Elementor's preview reload behaviour.
- **Composes Lite's `Helper`** (line 14 import).
- **Render at line 1596** — outputs both content blocks; JS swaps visibility based on switch state. All content renders in DOM (not lazy-loaded).

## Render Output

```html
<div class="eael-toggle-wrap [layout-class]">
  <!-- Switch -->
  <div class="eael-toggle-switch">
    <span class="eael-toggle-label primary active">{primary_label}</span>
    <label class="switch">
      <input type="checkbox" />
      <span class="slider"></span>
    </label>
    <span class="eael-toggle-label secondary">{secondary_label}</span>
  </div>
  <!-- Primary content (initially visible) -->
  <div class="eael-toggle-content primary active">
    <!-- Based on primary_content_type -->
    [?] {Elementor template rendered}
    [?] {WYSIWYG content}
    [?] <img src="{image}" />
  </div>
  <!-- Secondary content (initially hidden) -->
  <div class="eael-toggle-content secondary">
    <!-- Same three options as primary -->
  </div>
</div>
```

## Controls Reference

| Control id | Tab → Section | Type | Purpose |
| --- | --- | --- | --- |
| `primary_label` | Content → Switch | TEXT | Label left of switch |
| `secondary_label` | Content → Switch | TEXT | Label right of switch |
| `primary_content_type` | Content → Primary | SELECT | `template` / `content` / `image` |
| `primary_templates` | Content → Primary | SELECT | Elementor template (when type = template) |
| `primary_content` | Content → Primary | WYSIWYG | Rich content (when type = content) |
| `primary_image` | Content → Primary | MEDIA | Image (when type = image) |
| `secondary_*` mirror of primary | Content → Secondary | various | Same 3 options |
| Switch styling | Style → Switch | various | Position / color / size |

## Conditional Dependencies

```text
primary_content_type = 'template'
  └── shows primary_templates SELECT

primary_content_type = 'content'
  └── shows primary_content WYSIWYG

primary_content_type = 'image'
  └── shows primary_image MEDIA picker

(same for secondary_*)
```

## JavaScript Lifecycle

`src/js/view/toggle.js`:

```js
var ToggleHandler = function( $scope, $ ) {
    var $wrap = $scope.find( '.eael-toggle-wrap' );
    $wrap.find( 'input[type=checkbox]' ).on( 'change', function() {
        var state = this.checked ? 'secondary' : 'primary';
        $wrap.find( '.eael-toggle-content' ).removeClass( 'active' );
        $wrap.find( '.eael-toggle-content.' + state ).addClass( 'active' );
        $wrap.find( '.eael-toggle-label' ).removeClass( 'active' );
        $wrap.find( '.eael-toggle-label.' + state ).addClass( 'active' );
    } );
};
```

## Hooks & Filters

Standard widget render. No Pro-emitted hooks.

## Common Issues

| Symptom | Likely cause | Diagnose | Fix |
| --- | --- | --- | --- |
| Both content blocks visible | CSS `.active` rule missing | Inspect computed display | Verify Pro toggle.min.css loaded |
| Template doesn't render | Template ID invalid or template trashed | Inspect `primary_templates` setting | Re-pick a valid Elementor template |
| Switch doesn't toggle on tap (mobile) | Touch event mismatch | DevTools mobile mode | Confirm `<input type=checkbox>` is clickable (no overlay capturing taps) |
| State persists across reloads | No localStorage persistence | — | Expected. Add state-persistence in custom JS if needed. |
| Image too large | No image-size control | Inspect rendered `<img>` | Use Pro image-size control |

## Known Limitations

- **All content renders in DOM** — both primary and secondary content render server-side, even though only one is initially visible. Large templates = bulky HTML.
- **No keyboard accessibility audit** — `<input type=checkbox>` has built-in keyboard support but visual swap may not be announced to screen readers
- **State doesn't persist** — page reload resets to primary
- **No "deep link to state" support** — can't link a URL that opens with secondary active
- **Saved-template rendering** depends on Elementor's `frontend->get_builder_content_for_display( $template_id )` — verify call in `render()` for proper template hydration
- **Three content types × two states = 6 sub-control branches** — verbose `register_controls`

## Cross-References

- Used heavily with [`multicolumn-pricing-table.md`](multicolumn-pricing-table.md) and [`pricing-slider.md`](pricing-slider.md) for monthly/annual toggle UX
- Shared patterns: [`_patterns.md`](_patterns.md)
