Velo Example: Coding a More Robust Custom Element
- What This Article Will Demonstrate
- Display a Custom Element
- Add the Custom Element in the Wix Editor
- Modify the Behavior of the Custom Element
- Implement the Behaviors on Your Server
- Set Up Variables and Styles
- Define Functions
- Create the Custom Element Class
- Register the Custom Element
- The Custom Element Complete Code
- Code the Behaviors with Velo
- On One Page
- On All Site Pages
What This Article Will Demonstrate

Display a Custom Element
1const LOGO_TXT = 'https://static.wixstatic.com/media/logo-txt.png';
2const LOGO_IMG = 'https://static.wixstatic.com/media/logo-img.png';
3
4const createImg = (id, src) => {
5 const img = document.createElement('img');
6 img.src = src;
7 img.id = id;
8 img.width = '200';
9 img.style.position = 'fixed';
10 return img;
11}
12
13class myLogoDisplay extends HTMLElement {
14 constructor() {
15 super();
16 }
17
18 connectedCallback() {
19 this.appendChild(createImg('txt', LOGO_TXT));
20 this.appendChild(createImg('img', LOGO_IMG));
21 }
22
23}
24
25customElements.define('my-logo-display', myLogoDisplay);
To review:
- We created variables for the different parts of the logo custom element. We separated the logo into two images so that we can manipulate each one separately.
- We used the
connectedCallback()
function to display the logo images when the page is connected and ready. - We registered the custom element with
customElements.define()
.
Add the Custom Element in the Wix Editor
Because this example is using Velo, we can use Velo to host our custom element.
- Upload the custom element's JavaScript file to the Public > custom-elements folder in the Site Structure sidebar.
- Add the custom element to our page.
- In Settings, choose Velo File and select the file you uploaded.

Modify the Behavior of the Custom Element


Implement the Behaviors on Your Server
Set Up Variables and Styles
We first create three images that together comprise the company logo: LOGO_TXT
, LOGO_IMG
, and LOGO_PHONE
. By separating the logo into three parts, each part of the logo can be controlled separately. Each of these images are stored in the Wix Media Gallery.
We also create a STYLE
variable that defines custom element behaviors using CSS properties.
- The
animation
property sets multiple animations at one time as a shortcut. - The
animation-iteration-count
property sets how many times an animation should repeat. - The
transform
property is for rotating and resizing elements.
1const LOGO_TXT = 'https://static.wixstatic.com/media/logo-txt.png';
2const LOGO_IMG = 'https://static.wixstatic.com/media/logo-img.png';
3const LOGO_PHONE ='https://static.wixstatic.com/media/logo-phone.png';
4
5const STYLE = `
6#img.shake {
7 animation: shake 0.5s;
8 animation-iteration-count: infinite;
9 }
10
11 @keyframes shake {
12 0% { transform: translate(1px, 1px) rotate(0deg); }
13 10% { transform: translate(-1px, -2px) rotate(-1deg); }
14 20% { transform: translate(-3px, 0px) rotate(1deg); }
15 30% { transform: translate(3px, 2px) rotate(0deg); }
16 40% { transform: translate(1px, -1px) rotate(1deg); }
17 50% { transform: translate(-1px, 2px) rotate(-1deg); }
18 60% { transform: translate(-3px, 1px) rotate(0deg); }
19 70% { transform: translate(3px, 1px) rotate(-1deg); }
20 80% { transform: translate(-1px, -1px) rotate(1deg); }
21 90% { transform: translate(1px, 2px) rotate(0deg); }
22 100% { transform: translate(1px, -2px) rotate(-1deg); }
23 }
24`;
Define Functions
The scrollHandler()
function rotates the logo and changes its size.
1const scrollHandler = root => (event) => {
2 const txt = root.querySelector('#txt');
3 const scaleTxt = (Math.max(0, 1000 - window.scrollY)) / 1000;
4 txt.style.transform = `scale(${scaleTxt})`;
5 dispatchSizeChange(root, scaleTxt);
6 const opacityTxt = 1 / Math.log10(Math.max(10, window.scrollY));
7 txt.style.opacity = opacityTxt;
8 const img = root.querySelector('#img');
9 img.style.transform = `rotate(${window.scrollY / 10}deg)`;
10}
In our example, as we scroll up or down, our logo changes size.
This function checks if the element is getting smaller or bigger, and fires a sizeChange
event for Velo to handle. The dispatchEvent()
function creates the new custom event. The event is triggered when the size of the custom element gets bigger or smaller.
Later, when we code in Velo, we use the Velo API on()
function to handle the sizeChange
event.
1const dispatchSizeChange = (root,scaleTxt) => {
2 const gettingSmall = scaleTxt === 0 && root.prevScaleTxt !== 0;
3 const gettingBig = scaleTxt !== 0 && root.prevScaleTxt === 0;
4 root.prevScaleTxt = scaleTxt;
5
6 // Use dispatchEvent() to create a new custom event that
7 // Velo can handle. In our example, the event is
8 // triggered when the size of the custom element gets bigger or
9 // smaller.
10 if (gettingSmall || gettingBig) {
11 root.dispatchEvent(new CustomEvent('sizeChange',
12 { detail: { data: gettingSmall ? 'small' : 'big' } }));
13 }
14};
The createImg()
function takes each part of the logo and adds it to the page. This is equivalent to adding the image to the page's DOM.
1// Create each image that the logo comprises and
2// add it to the DOM using a createImg function.
3const createImg = (id, src) => {
4 const img = document.createElement('img');
5 img.src = src;
6 img.id = id;
7 img.width = '200';
8 img.style.position = 'fixed';
9 return img;
10}
Create the Custom Element Class
This code defines the allEffects
custom element class.
-
The class is designed to listen for the mouse wheel to indicate the page is being scrolled, and calls the
scrollHandler()
function using the style we defined earlier. In this example, if the visitor scrolls to the bottom of the page, the logo gets smaller. If the visitor scrolls to the top, the logo gets larger. -
Classes can also contain lifecycle callback functions. Lifecycle callback functions are triggered at specific, predetermined points. This code uses the
connectedCallback()
function, which is called when the custom element is available for rendering on the site. At this point, our custom element is displayed on the page, without the phone number. -
Another lifecycle callback function used in this code is the
attributeChangedCallback()
function. This function is called whenever an attribute of the custom element is added, removed, or changed. This function works together with a staticget observedAttributes()
method to determine what attributes changed. In this code, we check if the site visitor's mouse is hovering over the menu ("hoveringmenu"), and we check if the footer is currently displayed in the site visitor's viewport ("footershown"). If the mouse is over the menu, our logo shakes. If the footer is in view, the phone number is not shown.
1class allEffects extends HTMLElement {
2 constructor() {
3 super();
4 document.addEventListener('wheel', scrollHandler(this), {
5 capture: false, passive: true });
6 }
7
8 connectedCallback() {
9 this.appendChild(createImg('txt', LOGO_TXT));
10 this.appendChild(createImg('img', LOGO_IMG));
11 const phone = createImg('phone', LOGO_PHONE);
12 phone.style.display = 'none';
13 this.appendChild(phone);
14
15 const style = document.createElement('style');
16 style.innerHTML = STYLE;
17 this.appendChild(style);
18 }
19
20 attributeChangedCallback(name, oldValue, newValue) {
21 if (name === 'hoveringmenu') {
22 this.querySelector('#img').className = newValue === 'true' ? 'shake' : '';
23 }
24 if (name === 'footershown') {
25 this.querySelector('#phone').style.display = newValue === 'true' ? '' : 'none';
26 }
27 }
28
29 static get observedAttributes() {
30 return ['hoveringmenu', 'footershown'];
31 }
32}
Register the Custom Element
This part of the code actually defines the custom element that we specified using the parameters, functions, and class above. Use the customElements.define
method to register the custom element class allEffects
, and associate the custom element with the tag name for the custom element in the Velo representation of the custom element,my-logo-effects
.
1customElements.define('my-logo-effects', allEffects);
The Custom Element Complete Code
1const LOGO_TXT = 'https://static.wixstatic.com/media/logo-txt.png';
2const LOGO_IMG = 'https://static.wixstatic.com/media/logo-img.png';
3const LOGO_PHONE ='https://static.wixstatic.com/media/logo-phone.png';
4
5const STYLE = `
6#img.shake {
7 animation: shake 0.5s;
8 animation-iteration-count: infinite;
9 }
10
11 @keyframes shake {
12 0% { transform: translate(1px, 1px) rotate(0deg); }
13 10% { transform: translate(-1px, -2px) rotate(-1deg); }
14 20% { transform: translate(-3px, 0px) rotate(1deg); }
15 30% { transform: translate(3px, 2px) rotate(0deg); }
16 40% { transform: translate(1px, -1px) rotate(1deg); }
17 50% { transform: translate(-1px, 2px) rotate(-1deg); }
18 60% { transform: translate(-3px, 1px) rotate(0deg); }
19 70% { transform: translate(3px, 1px) rotate(-1deg); }
20 80% { transform: translate(-1px, -1px) rotate(1deg); }
21 90% { transform: translate(1px, 2px) rotate(0deg); }
22 100% { transform: translate(1px, -2px) rotate(-1deg); }
23 }
24`;
25
26const scrollHandler = root => (event) => {
27 const txt = root.querySelector('#txt');
28 const scaleTxt = (Math.max(0, 1000 - window.scrollY)) / 1000;
29 txt.style.transform = `scale(${scaleTxt})`;
30 dispatchSizeChange(root, scaleTxt);
31 const opacityTxt = 1 / Math.log10(Math.max(10, window.scrollY));
32 txt.style.opacity = opacityTxt;
33 const img = root.querySelector('#img');
34 img.style.transform = `rotate(${window.scrollY / 10}deg)`;
35}
36
37const dispatchSizeChange = (root,scaleTxt) => {
38 const gettingSmall = scaleTxt === 0 && root.prevScaleTxt !== 0;
39 const gettingBig = scaleTxt !== 0 && root.prevScaleTxt === 0;
40 root.prevScaleTxt = scaleTxt;
41
42 if (gettingSmall || gettingBig) {
43 root.dispatchEvent(new CustomEvent('sizeChange',
44 { detail: { data: gettingSmall ? 'small' : 'big' } }));
45 }
46};
47
48const createImg = (id, src) => {
49 const img = document.createElement('img');
50 img.src = src;
51 img.id = id;
52 img.width = '200';
53 img.style.position = 'fixed';
54 return img;
55}
56
57class allEffects extends HTMLElement {
58 constructor() {
59 super();
60 document.addEventListener('wheel', scrollHandler(this), {
61 capture: false, passive: true });
62 }
63
64 connectedCallback() {
65 this.appendChild(createImg('txt', LOGO_TXT));
66 this.appendChild(createImg('img', LOGO_IMG));
67 const phone = createImg('phone', LOGO_PHONE);
68 phone.style.display = 'none';
69 this.appendChild(phone);
70
71 const style = document.createElement('style');
72 style.innerHTML = STYLE;
73 this.appendChild(style);
74 }
75
76 attributeChangedCallback(name, oldValue, newValue) {
77 if (name === 'hoveringmenu') {
78 this.querySelector('#img').className = newValue === 'true' ? 'shake' : '';
79 }
80 if (name === 'footershown') {
81 this.querySelector('#phone').style.display = newValue === 'true' ? '' : 'none';
82 }
83 }
84
85 static get observedAttributes() {
86 return ['hoveringmenu', 'footershown'];
87 }
88}
89
90customElements.define('my-logo-effects', allEffects);
Code the Behaviors with Velo
You can customize how the custom element behaves for a single page on the site or for all pages on the site.
On One Page
In our example, use the on()
function to define what happens when the sizeChange
event is triggered.
As we scroll down the current page:
- The logo gets smaller, as defined in the custom element's Javascript file.
- The menu disappears, as defined here in Velo.
We defined this sizeChange
event in the Javascript file that contains our custom element's implementation in Line 11
above: new CustomEvent('sizeChange')
;
Creating events is the custom element's way of "communicating" with Velo. Events let Velo know that something has to be handled, and parameters can be sent with the event.
1$w.onReady(function () {
2 $w('#myCustomElement').on('sizeChange', ({detail: {data}}) => {
3 if (data === 'small') {
4 $w('#menu').hide();
5 } else {
6 $w('#menu').show();
7 }
8 })
9});
On All Site Pages
In the following code, we create events for the actions that a site visitor might perform that impact our custom element.
We then create and set attributes on the custom element using setAttribute()
. This is Velo's way of "communicating" with the custom element. We can pass the custom element attributes and their values so that the custom element's implementation can behave accordingly.
- If the footer enters or leaves the viewport.
- If the site visitor's mouse hovers or stops hovering over the menu.
1$w.onReady(function () {
2 $w('#footer').onViewportEnter((event) => {
3 $w('#myCustomElement').setAttribute('footershown', true);
4 });
5
6 $w('#footer').onViewportLeave((event) => {
7 $w('#myCustomElement').setAttribute('footershown', false);
8 });
9
10 $w('#menu').onMouseIn((event) => {
11 $w('#myCustomElement').setAttribute('hoveringmenu', true);
12 });
13
14 $w('#menu').onMouseOut((event) => {
15 $w('myCustomElement').setAttribute('hoveringmenu', false);
16 });
17});