Multiple Widget Instances Fix
March 26, 2025 Β· AI Conversations
<style>
@font-face {
src: url('https://res.cloudinary.com/dr6lvwubh/raw/upload/v1529908256/CompressaPRO-GX.woff2');
font-family:'Compressa VF';
font-style: normal;
}
#fit h1 {
font-family:'Compressa VF';
text-rendering: optimizeSpeed;
color: #b6e925;
/* width: 100%;*/
user-select: none;
line-height: 0.8em;
margin: 0 auto;
text-transform: uppercase;
font-weight: 100;
text-align: center;
/* width: 100vw;*/
}
#fit h1 span {
transform: translateY(-10px);
user-select: none;
font-family:'Compressa VF';
}
#fit h1.flex {
display: flex;
justify-content: space-between;
}
#fit h1.stroke span {
position: relative;
color: #211D26;
line-height: inherit;
}
#fit h1.stroke span:after {
content: attr(data-char);
-webkit-text-stroke-width: 3px;
-webkit-text-stroke-color: #FE6730;
position: absolute;
left: 0;
line-height: inherit;
color: transparent;
z-index: -1;
}
</style>
<script>
</script>
<script>
var maxDist;
var mouse = { x: 0, y: 0 };
var cursor = {
x: window.innerWidth,
y: window.innerHeight
};
Math.dist = function(a, b) {
var dx = b.x - a.x;
var dy = b.y - a.y;
return Math.sqrt(Math.pow(dx, 2), Math.pow(dy, 2));
}
window.addEventListener("mousemove", function(e) {
cursor.x = e.clientX;
cursor.y = e.clientY;
});
window.addEventListener("touchmove", function(e) {
var t = e.touches[0];
cursor.x = t.clientX;
cursor.y = t.clientY;
}, {
passive: false
});
var Char = function(container, char) {
var span = document.createElement("span");
span.setAttribute('data-char', char);
span.innerText = char;
container.appendChild(span);
this.getDist = function() {
this.pos = span.getBoundingClientRect();
return Math.dist(mouse, {
x: this.pos.x + (this.pos.width / 1.75),
y: this.pos.y
});
}
this.getAttr = function(dist, min, max) {
var wght = max - Math.abs((max * dist / maxDist));
return Math.max(min, wght + min);
}
this.update = function(args) {
var dist = this.getDist();
this.wdth = args.wdth ? ~~this.getAttr(dist, 5, 200) : 100;
this.wght = args.wght ? ~~this.getAttr(dist, 100, 800) : 400;
this.alpha = args.alpha ? this.getAttr(dist, 0, 1).toFixed(2) : 1;
this.ital = args.ital ? this.getAttr(dist, 0, 1).toFixed(2) : 0;
this.draw();
}
this.draw = function() {
var style = "";
style += "opacity: " + this.alpha + ";";
style += "font-variation-settings: 'wght' " + this.wght + ", 'wdth' " + this.wdth + ", 'ital' " + this.ital + ";";
span.style = style;
}
return this;
}
var VFont = function() {
this.scale = false;
this.flex = true;
this.alpha = false;
this.stroke = false;
this.width = true;
this.weight = true;
this.italic = true;
var title, str, chars = [];
this.init = function() {
title = document.getElementById("title");
str = title.innerText;
title.innerHTML = "";
for (var i = 0; i < str.length; i++) {
var _char = new Char(title, str[i]);
chars.push(_char);
}
this.set();
window.addEventListener("resize", this.setSize.bind(this));
}
this.set = function() {
title.className = "";
title.className += this.flex ? " flex" : "";
title.className += this.stroke ? " stroke" : "";
this.setSize();
}
this.setSize = function() {
var mainDiv = document.getElementById("fit"); // assuming the outer div has an id "main"
var fontSize = mainDiv.clientWidth / (str.length / 2);
title.style = "font-size: " + fontSize + "px;";
if (this.scale) {
var scaleY = (window.innerHeight / title.getBoundingClientRect().height).toFixed(2);
var lineHeight = scaleY * 0.8;
title.style = "font-size: " + fontSize + "px; transform: scale(1," + scaleY + "); line-height: " + lineHeight + "em;"
}
}
this.animate = function() {
mouse.x += (cursor.x - mouse.x) / 20;
mouse.y += (cursor.y - mouse.y) / 20;
requestAnimationFrame(this.animate.bind(this));
this.render();
}
this.render = function() {
maxDist = title.getBoundingClientRect().width / 2;
for (var i = 0; i < chars.length; i++) {
chars[i].update({
wght: this.weight,
wdth: this.width,
ital: this.italic,
alpha: this.alpha
});
}
}
this.init();
this.animate();
return this;
}
var txt = new VFont();
// Start animation
txt.animate();
</script>
—
### π **Issue Breakdown**
1. **ID Conflicts**:
– The HTML uses static IDs: `id="fit"` and `id="title"` β which means **multiple widgets will reuse the same ID**, leading to unpredictable DOM behavior and broken animations.
2. **JavaScript is not modular**:
– The JS initializes the animation for a single `#title` inside `#fit`, and does not loop through multiple instances.
—
### β **Solution Overview**
To make this work with **multiple widget instances on the same page**, you need to:
– Use **unique IDs** or **classes with `data-*` attributes**.
– Adjust the **JavaScript** to loop over all instances and apply the animation individually.
—
### β **Updated Widget Render Method**
Hereβs how you can update your `render()` function (and JS) to handle multiple instances correctly:
protected function render() {
$settings = $this->get_settings_for_display();
$text = $settings['content'];
$effect_class = esc_attr($settings['font_effect']);
$unique_id = 'variable-font-' . $this->get_id();
echo '<div class="variable-font-wrapper ' . $effect_class . '" id="' . esc_attr($unique_id) . '" data-uid="' . esc_attr($unique_id) . '">';
echo ' <h1 class="variable-font-title">' . esc_html($text) . '</h1>';
echo '</div>';
?>
<style>
@font-face {
src: url('https://res.cloudinary.com/dr6lvwubh/raw/upload/v1529908256/CompressaPRO-GX.woff2');
font-family:'Compressa VF';
font-style: normal;
}
.variable-font-title {
font-family:'Compressa VF';
text-rendering: optimizeSpeed;
color: #b6e925;
user-select: none;
line-height: 0.8em;
margin: 0 auto;
text-transform: uppercase;
font-weight: 100;
text-align: center;
}
.variable-font-title span {
transform: translateY(-10px);
user-select: none;
font-family:'Compressa VF';
}
.variable-font-title.flex {
display: flex;
justify-content: space-between;
}
.variable-font-title.stroke span {
position: relative;
color: #211D26;
line-height: inherit;
}
.variable-font-title.stroke span:after {
content: attr(data-char);
-webkit-text-stroke-width: 3px;
-webkit-text-stroke-color: #FE6730;
position: absolute;
left: 0;
line-height: inherit;
color: transparent;
z-index: -1;
}
</style>
<script>
(function(){
const wrapper = document.getElementById("<?php echo esc_js($unique_id); ?>");
const title = wrapper.querySelector(".variable-font-title");
const str = title.innerText;
const chars = [];
let mouse = { x: 0, y: 0 };
let cursor = { x: window.innerWidth, y: window.innerHeight };
let maxDist;
title.innerHTML = '';
[...str].forEach(char => {
const span = document.createElement("span");
span.setAttribute('data-char', char);
span.innerText = char;
title.appendChild(span);
chars.push(span);
});
function getDist(span) {
const rect = span.getBoundingClientRect();
return Math.hypot((rect.left + rect.width / 2) - mouse.x, (rect.top + rect.height / 2) - mouse.y);
}
function getAttr(dist, min, max) {
const val = max - Math.abs((max * dist / maxDist));
return Math.max(min, val + min);
}
function updateChar(span) {
const dist = getDist(span);
const wdth = getAttr(dist, 5, 200);
const wght = getAttr(dist, 100, 800);
const ital = getAttr(dist, 0, 1).toFixed(2);
span.style.fontVariationSettings = `'wght' ${~~wght}, 'wdth' ${~~wdth}, 'ital' ${ital}`;
}
function render() {
maxDist = title.getBoundingClientRect().width / 2;
chars.forEach(span => updateChar(span));
requestAnimationFrame(render);
}
window.addEventListener("mousemove", function(e) {
cursor.x = e.clientX;
cursor.y = e.clientY;
});
window.addEventListener("touchmove", function(e) {
const t = e.touches[0];
cursor.x = t.clientX;
cursor.y = t.clientY;
}, { passive: false });
function animateMouse() {
mouse.x += (cursor.x - mouse.x) / 20;
mouse.y += (cursor.y - mouse.y) / 20;
requestAnimationFrame(animateMouse);
}
animateMouse();
render();
})();
</script>
<?php
}
—
### π― Key Improvements
– Each widget instance gets a **unique `ID`** via `$this->get_id()`.
– The JS is **self-contained** in an IIFE (`(function(){…})()`) to avoid global variable conflicts.
– It loops over the characters of the specific widget's text only.
– It tracks mouse movement globally, but calculates distance per widget instance.
—
Let me know if you'd like to convert this to use an **external JS file** or hook into Elementor's lifecycle like `elementor/frontend/init`.
<style>
@font-face {
src: url('https://res.cloudinary.com/dr6lvwubh/raw/upload/v1529908256/CompressaPRO-GX.woff2');
font-family:'Compressa VF';
font-style: normal;
}
#fit h1 {
font-family:'Compressa VF';
text-rendering: optimizeSpeed;
color: #b6e925;
/* width: 100%;*/
user-select: none;
line-height: 0.8em;
margin: 0 auto;
text-transform: uppercase;
font-weight: 100;
text-align: center;
/* width: 100vw;*/
}
#fit h1 span {
transform: translateY(-10px);
user-select: none;
font-family:'Compressa VF';
}
#fit h1.flex {
display: flex;
justify-content: space-between;
}
#fit h1.stroke span {
position: relative;
color: #211D26;
line-height: inherit;
}
#fit h1.stroke span:after {
content: attr(data-char);
-webkit-text-stroke-width: 3px;
-webkit-text-stroke-color: #FE6730;
position: absolute;
left: 0;
line-height: inherit;
color: transparent;
z-index: -1;
}
</style>
<script>
</script>
<script>
var maxDist;
var mouse = { x: 0, y: 0 };
var cursor = {
x: window.innerWidth,
y: window.innerHeight
};
Math.dist = function(a, b) {
var dx = b.x - a.x;
var dy = b.y - a.y;
return Math.sqrt(Math.pow(dx, 2), Math.pow(dy, 2));
}
window.addEventListener("mousemove", function(e) {
cursor.x = e.clientX;
cursor.y = e.clientY;
});
window.addEventListener("touchmove", function(e) {
var t = e.touches[0];
cursor.x = t.clientX;
cursor.y = t.clientY;
}, {
passive: false
});
var Char = function(container, char) {
var span = document.createElement("span");
span.setAttribute('data-char', char);
span.innerText = char;
container.appendChild(span);
this.getDist = function() {
this.pos = span.getBoundingClientRect();
return Math.dist(mouse, {
x: this.pos.x + (this.pos.width / 1.75),
y: this.pos.y
});
}
this.getAttr = function(dist, min, max) {
var wght = max - Math.abs((max * dist / maxDist));
return Math.max(min, wght + min);
}
this.update = function(args) {
var dist = this.getDist();
this.wdth = args.wdth ? ~~this.getAttr(dist, 5, 200) : 100;
this.wght = args.wght ? ~~this.getAttr(dist, 100, 800) : 400;
this.alpha = args.alpha ? this.getAttr(dist, 0, 1).toFixed(2) : 1;
this.ital = args.ital ? this.getAttr(dist, 0, 1).toFixed(2) : 0;
this.draw();
}
this.draw = function() {
var style = "";
style += "opacity: " + this.alpha + ";";
style += "font-variation-settings: 'wght' " + this.wght + ", 'wdth' " + this.wdth + ", 'ital' " + this.ital + ";";
span.style = style;
}
return this;
}
var VFont = function() {
this.scale = false;
this.flex = true;
this.alpha = false;
this.stroke = false;
this.width = true;
this.weight = true;
this.italic = true;
var title, str, chars = [];
this.init = function() {
title = document.getElementById("title");
str = title.innerText;
title.innerHTML = "";
for (var i = 0; i < str.length; i++) {
var _char = new Char(title, str[i]);
chars.push(_char);
}
this.set();
window.addEventListener("resize", this.setSize.bind(this));
}
this.set = function() {
title.className = "";
title.className += this.flex ? " flex" : "";
title.className += this.stroke ? " stroke" : "";
this.setSize();
}
this.setSize = function() {
var mainDiv = document.getElementById("fit"); // assuming the outer div has an id "main"
var fontSize = mainDiv.clientWidth / (str.length / 2);
title.style = "font-size: " + fontSize + "px;";
if (this.scale) {
var scaleY = (window.innerHeight / title.getBoundingClientRect().height).toFixed(2);
var lineHeight = scaleY * 0.8;
title.style = "font-size: " + fontSize + "px; transform: scale(1," + scaleY + "); line-height: " + lineHeight + "em;"
}
}
this.animate = function() {
mouse.x += (cursor.x - mouse.x) / 20;
mouse.y += (cursor.y - mouse.y) / 20;
requestAnimationFrame(this.animate.bind(this));
this.render();
}
this.render = function() {
maxDist = title.getBoundingClientRect().width / 2;
for (var i = 0; i < chars.length; i++) {
chars[i].update({
wght: this.weight,
wdth: this.width,
ital: this.italic,
alpha: this.alpha
});
}
}
this.init();
this.animate();
return this;
}
var txt = new VFont();
// Start animation
txt.animate();
</script>
### β Here's the **corrected version** that:
– Avoids duplicate `id` conflicts
– Makes the JavaScript **instance-safe**
– Works with **multiple widget instances** on a page
—
### β
**Updated `render()` Function**
Replace your `render()` method with this:
protected function render() {
$settings = $this->get_settings_for_display();
$text = $settings['content'];
$effect_class = esc_attr($settings['font_effect']);
$unique_id = 'variable-font-' . esc_attr($this->get_id()); // Unique ID per widget
echo '<style>
@font-face {
src: url("https://res.cloudinary.com/dr6lvwubh/raw/upload/v1529908256/CompressaPRO-GX.woff2");
font-family: "Compressa VF";
font-style: normal;
}
#' . $unique_id . ' h1 {
font-family: "Compressa VF";
text-rendering: optimizeSpeed;
color: #b6e925;
user-select: none;
line-height: 0.8em;
margin: 0 auto;
text-transform: uppercase;
font-weight: 100;
text-align: center;
}
#' . $unique_id . ' h1 span {
transform: translateY(-10px);
user-select: none;
}
#' . $unique_id . ' h1.flex {
display: flex;
justify-content: space-between;
}
#' . $unique_id . ' h1.stroke span {
position: relative;
color: #211D26;
line-height: inherit;
}
#' . $unique_id . ' h1.stroke span:after {
content: attr(data-char);
-webkit-text-stroke-width: 3px;
-webkit-text-stroke-color: #FE6730;
position: absolute;
left: 0;
line-height: inherit;
color: transparent;
z-index: -1;
}
</style>';
echo "<div id='{$unique_id}' class='variable-font-wrapper {$effect_class}'>";
echo " <h1 class='variable-font-title'>{$text}</h1>";
echo "</div>";
// Inline script per widget instance
echo "<script>
(function(){
const wrapper = document.getElementById('{$unique_id}');
if (!wrapper) return;
const title = wrapper.querySelector('.variable-font-title');
const str = title.innerText;
const chars = [];
title.innerHTML = '';
for (let i = 0; i < str.length; i++) {
const span = document.createElement('span');
span.setAttribute('data-char', str[i]);
span.innerText = str[i];
title.appendChild(span);
chars.push(span);
}
let mouse = { x: 0, y: 0 };
let cursor = { x: window.innerWidth, y: window.innerHeight };
let maxDist = title.getBoundingClientRect().width / 2;
window.addEventListener('mousemove', function(e) {
cursor.x = e.clientX;
cursor.y = e.clientY;
});
window.addEventListener('touchmove', function(e) {
const t = e.touches[0];
cursor.x = t.clientX;
cursor.y = t.clientY;
}, { passive: false });
function getDist(span) {
const rect = span.getBoundingClientRect();
return Math.hypot((rect.left + rect.width / 2) - mouse.x, (rect.top + rect.height / 2) - mouse.y);
}
function getAttr(dist, min, max) {
const val = max - Math.abs((max * dist / maxDist));
return Math.max(min, val + min);
}
function updateChar(span) {
const dist = getDist(span);
const wdth = getAttr(dist, 5, 200);
const wght = getAttr(dist, 100, 800);
const ital = getAttr(dist, 0, 1).toFixed(2);
span.style.fontVariationSettings = `'wght' ${~~wght}, 'wdth' ${~~wdth}, 'ital' ${ital}`;
}
function animateMouse() {
mouse.x += (cursor.x - mouse.x) / 20;
mouse.y += (cursor.y - mouse.y) / 20;
requestAnimationFrame(animateMouse);
}
function render() {
maxDist = title.getBoundingClientRect().width / 2;
chars.forEach(span => updateChar(span));
requestAnimationFrame(render);
}
// Resize handler to update font size
function setFontSize() {
const width = wrapper.clientWidth;
const fontSize = width / (str.length / 2);
title.style.fontSize = fontSize + 'px';
}
setFontSize();
window.addEventListener('resize', setFontSize);
// Start animations
animateMouse();
render();
})();
</script>";
}
—
### β Final Notes
– **No more `id="fit"` or `id="title"` reuse** β everything is scoped by the widget's unique ID.
– JavaScript runs **independently** per instance.
– Fully safe for **multiple widget usage** on the same Elementor page.
Let me know if you want to load the script externally or conditionally enqueue only once per page.
$this->get_id();
This function returns a **unique ID** for each widget on the page, even if it's the same widget type used multiple times.
—
### β How to Use in Your `render()` Function
Hereβs how you assign a unique ID for each widget instance:
$unique_id = 'variable-font-' . esc_attr($this->get_id());
You can then apply this `$unique_id` to your wrapping `div`, like:
echo "<div id='{$unique_id}' class='variable-font-wrapper'>";
And reference it consistently in your “ and “ tags:
echo "<style>
#{$unique_id} h1 {
font-family: 'Compressa VF';
...
}
</style>";
echo "<script>
(function(){
const wrapper = document.getElementById('{$unique_id}');
...
})();
</script>";
—
### β Where to Use `get_id()`:
– `id` attributes
– `querySelector`, `getElementById`, etc. in JS
– CSS selectors inside `style` tag or in JS `querySelector`
– Avoid hardcoded `#fit`, `#title` β use the unique ID instead
—
### Example Output in DOM:
<div id="variable-font-abc123" class="variable-font-wrapper">
<h1 class="variable-font-title">My Animated Text</h1>
</div>
The ID `variable-font-abc123` is unique to this widget instance.
—
Let me know if you want this to be added as a `data-uid` attribute too for even cleaner JS handling or if you're planning to enqueue the script globally instead of inline.
<script>
</script>
include this only once in elementor widget
—
### β Best Practice: Use `ElementorPlugin::instance()->frontend->add_render_attribute()`
But for simplicity and full control, hereβs how to **conditionally load the script once per page** using a **static variable** inside your widget class:
—
### β Modify Your Widget Class Like This
class Elementor_Variable_Font_Widget extends ElementorWidget_Base {
// Static property to track if script was already printed
protected static $script_loaded = false;
...
protected function render() {
$settings = $this->get_settings_for_display();
$unique_id = 'variable-font-' . esc_attr($this->get_id());
$text = esc_html($settings['content']);
echo "<div id='{$unique_id}' class='variable-font-wrapper'>";
echo " <h1 class='variable-font-title'>{$text}</h1>";
echo "</div>";
// β
Print this only ONCE
if ( ! self::$script_loaded ) {
echo '<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.5/dat.gui.min.js"></script>';
self::$script_loaded = true;
}
// Now continue with widget-specific script
echo "<script>
(function(){
const wrapper = document.getElementById('{$unique_id}');
if (!wrapper) return;
const title = wrapper.querySelector('.variable-font-title');
const str = title.innerText;
const chars = [];
title.innerHTML = '';
for (let i = 0; i < str.length; i++) {
const span = document.createElement('span');
span.setAttribute('data-char', str[i]);
span.innerText = str[i];
title.appendChild(span);
chars.push(span);
}
let mouse = { x: 0, y: 0 };
let cursor = { x: window.innerWidth, y: window.innerHeight };
let maxDist = title.getBoundingClientRect().width / 2;
window.addEventListener('mousemove', function(e) {
cursor.x = e.clientX;
cursor.y = e.clientY;
});
window.addEventListener('touchmove', function(e) {
const t = e.touches[0];
cursor.x = t.clientX;
cursor.y = t.clientY;
}, { passive: false });
function getDist(span) {
const rect = span.getBoundingClientRect();
return Math.hypot((rect.left + rect.width / 2) - mouse.x, (rect.top + rect.height / 2) - mouse.y);
}
function getAttr(dist, min, max) {
const val = max - Math.abs((max * dist / maxDist));
return Math.max(min, val + min);
}
function updateChar(span) {
const dist = getDist(span);
const wdth = getAttr(dist, 5, 200);
const wght = getAttr(dist, 100, 800);
const ital = getAttr(dist, 0, 1).toFixed(2);
span.style.fontVariationSettings = `'wght' ${~~wght}, 'wdth' ${~~wdth}, 'ital' ${ital}`;
}
function animateMouse() {
mouse.x += (cursor.x - mouse.x) / 20;
mouse.y += (cursor.y - mouse.y) / 20;
requestAnimationFrame(animateMouse);
}
function render() {
maxDist = title.getBoundingClientRect().width / 2;
chars.forEach(span => updateChar(span));
requestAnimationFrame(render);
}
function setFontSize() {
const width = wrapper.clientWidth;
const fontSize = width / (str.length / 2);
title.style.fontSize = fontSize + 'px';
}
setFontSize();
window.addEventListener('resize', setFontSize);
animateMouse();
render();
})();
</script>";
}
}
—
### π What's Happening:
– `static $script_loaded` is used to track whether the external script has been printed already.
– It **only prints “ once** β even if the widget is used 10 times.
– Elementor renders each widget instance separately, so this is a safe and performant way to manage third-party scripts inline.
—
Let me know if youβd prefer to register/enqueue it through WordPress properly instead of inline!