Advanced Whatsapp Widget

Hi everyone, I’d like to ask how I can make the widget look similar to the one on this website. https://healthi-life.com/ Does anyone have any ideas or suggestions?

Hi there, @Health_Passport :waving_hand:

Thanks for sharing an example! Just to confirm, you’d like to add a badge with the agent name, photo and online status next to the chat bubble, right?

yes and also the interface when clicking on Whatsapp icon as well.

Got it, thanks!

I am not sure if it’s possible to fully replicate this design. However, I’ve passed your request to the devs and, hopefully, they’ll be able to implement this idea.

I’ll keep you updated :slightly_smiling_face:

Hi @Health_Passport :waving_hand:

Thank you for waiting!

Here is a solution from the dev team:

  1. This code should be added to the Custom CSS field on the Appearance tab of your widget’s settings:
.custom-badge-root {
  box-shadow: 0 0 16px rgba(0, 0, 0, 0.25);
  display: inline-flex;
  padding: 6px;
  padding-right: 16px;
  border-radius: 32px;
  background: white;
  margin-right: 16px;
  align-items: center;
  gap: 8px;
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity 0.25s ease,
    transform 0.25s ease;
  pointer-events: auto;
}

.custom-badge-root-hidden {
  opacity: 0;
  transform: translateY(-16px);
  pointer-events: none;
}

.custom-badge-image-container {
  position: relative;
  width: 32px;
  height: 32px;
}

.custom-badge-image {
  border-radius: 50%;
  width: 100%;
  height: 100%;
}

.custom-badge-status {
  right: 0;
  bottom: 0;
  position: absolute;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: rgb(34, 197, 94);
  border: 1px white solid;
}

.custom-badge-text {
  font-size: 14px;
}

.custom-button-container {
  background: white;
  padding: 16px;
}

.custom-button {
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 14px;
  border-radius: 12px;
  transition: transform 0.2s ease;
  background: linear-gradient(135deg, rgb(37, 211, 102) 0%, rgb(18, 140, 126) 100%);
}

.custom-button:hover {
  transform: scale(1.05);
}

.custom-button-text {
  font-size: 16px;
  font-weight: 500;
  color: white;
}

.custom-close-icon {
  position: absolute;
  left: 50%;
  top: 50%;
  opacity: 1;
  transform: translate(-50%, -50%) rotate(0deg) scale(1);
  transition:
    opacity 0.25s ease,
    transform 0.25s ease !important;
}

.custom-close-icon-hidden {
  opacity: 0;
  transform: translate(-50%, -50%) rotate(-180deg) scale(0.6);
  pointer-events: none;
}
  1. This script should be placed in the Custom JS section on the Appearance tab of your widget’s settings:
const IMG_URL =
  'https://healthi-life.com/assets/images/concierge-anna-BChUVpiZ.jpg';
const BADGE_TEXT = 'Your Medical Concierge';
const BUTTON_TEXT = 'WhatsApp';

const ANIMATION_DURATION = 250;

const CLOSE_ICON_CLASSNAME = 'custom-close-icon';
const CLOSE_ICON_HIDDEN_CLASSNAME = 'custom-close-icon-hidden';

const BADGE_ROOT_CLASSNAME = 'custom-badge-root';
const BADGE_TEXT_CLASSNAME = 'custom-badge-text';
const BADGE_IMG_CONTAINER_CLASSNAME = 'custom-badge-image-container';
const BADGE_IMG_CLASSNAME = 'custom-badge-image';
const BADGE_STATUS_CLASSNAME = 'custom-badge-status';
const BADGE_HIDDEN_CLASSNAME = 'custom-badge-root-hidden';

const BUTTON_CLASSNAME = 'custom-button';
const BUTTON_CONTAINER_CLASSNAME = 'custom-button-container';
const BUTTON_ICON_CLASSNAME = 'custom-button-icon';
const BUTTON_TEXT_CLASSNAME = 'custom-button-text';

const getIsWindowOpen = (container) => {
  const chatWindow = container.querySelector('[class*="container"');
  const events = getComputedStyle(chatWindow).pointerEvents;

  return events === 'all' ? true : false;
};

const createButtonIcon = () => {
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');

  svg.setAttribute('width', '24px');
  svg.setAttribute('height', '24px');
  svg.setAttribute('viewBox', '0 0 24 24');
  svg.setAttribute('fill', 'white');
  svg.setAttribute('aria-hidden', 'true');

  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');

  path.setAttribute(
    'd',
    'M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z'
  );

  svg.append(path);
  svg.classList.add(BUTTON_ICON_CLASSNAME);

  return svg;
};

const createButton = (container) => {
  const rootContainer = document.createElement('div');
  rootContainer.className = BUTTON_CONTAINER_CLASSNAME;

  const root = document.createElement('div');
  root.className = BUTTON_CLASSNAME;

  const icon = createButtonIcon();

  const text = document.createElement('span');
  text.className = BUTTON_TEXT_CLASSNAME;
  text.textContent = BUTTON_TEXT;

  root.append(icon, text);
  rootContainer.append(root);

  const sendButton = container.querySelector(
    '[class*="MessageField__MessageButtonContainer-sc"] button'
  );

  root.addEventListener('click', () => sendButton.click());

  return rootContainer;
};

const createBadge = () => {
  const root = document.createElement('div');
  root.className = BADGE_ROOT_CLASSNAME;

  const text = document.createElement('span');
  text.className = BADGE_TEXT_CLASSNAME;
  text.textContent = BADGE_TEXT;

  const imgContainer = document.createElement('div');
  imgContainer.className = BADGE_IMG_CONTAINER_CLASSNAME;

  const img = document.createElement('img');
  img.className = BADGE_IMG_CLASSNAME;
  img.src = IMG_URL;
  img.alt = '';

  const statusBadge = document.createElement('div');
  statusBadge.className = BADGE_STATUS_CLASSNAME;

  imgContainer.append(img, statusBadge);
  root.append(imgContainer);
  root.append(text);

  return root;
};

const createCloseSVG = () => {
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');

  svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
  svg.setAttribute('width', '24px');
  svg.setAttribute('height', '24px');
  svg.setAttribute('viewBox', '0 0 24 24');
  svg.setAttribute('class', 'injected-svg');
  svg.setAttribute(
    'data-src',
    'https://api.iconify.design/material-symbols/close.svg'
  );

  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');

  path.setAttribute('fill', 'currentColor');
  path.setAttribute(
    'd',
    'M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6z'
  );

  svg.append(path);
  svg.classList.add(CLOSE_ICON_CLASSNAME);

  return svg;
};

const waitForElement = (selector, interval = 250) => {
  return new Promise((resolve) => {
    const timer = setInterval(() => {
      const element = util.findElement(selector);
      if (element) {
        clearInterval(timer);
        resolve(element);
      }
    }, interval);
  });
};

waitForElement('[class*="Main__Container-sc"]').then((container) => {
  waitForElement(
    '[class*="FloatingButton__FloatingButtonContainer-sc"] svg'
  ).then((bubbleIcon) => {
    waitForElement('[class*="Window__ChatGroup-sc').then((chat) => {
      const closeIcon = createCloseSVG();
      closeIcon.classList.add(CLOSE_ICON_HIDDEN_CLASSNAME);
      bubbleIcon.after(closeIcon);

      const button = container.querySelector(
        '[class*="FloatingButton__FloatingButtonContainer-sc"]'
      );
      const badge = createBadge();
      button.prepend(badge);

      const chatButton = createButton(container);
      chat.append(chatButton);

      const portalObs = new MutationObserver(() => {
        const isOpen = getIsWindowOpen(container);

        if (isOpen) {
          bubbleIcon.style.opacity = 0;

          if (container.querySelector(`.${CLOSE_ICON_HIDDEN_CLASSNAME}`)) {
            closeIcon.classList.remove(CLOSE_ICON_HIDDEN_CLASSNAME);
          }

          if (!container.querySelector(`.${BADGE_HIDDEN_CLASSNAME}`)) {
            badge.classList.add(BADGE_HIDDEN_CLASSNAME);
          }
        } else {
          setTimeout(() => {
            bubbleIcon.style.opacity = 1;
          }, ANIMATION_DURATION);

          if (!container.querySelector(`.${CLOSE_ICON_HIDDEN_CLASSNAME}`)) {
            closeIcon.classList.add(CLOSE_ICON_HIDDEN_CLASSNAME);
          }

          if (container.querySelector(`.${BADGE_HIDDEN_CLASSNAME}`)) {
            badge.classList.remove(BADGE_HIDDEN_CLASSNAME);
          }
        }
      });

      portalObs.observe(container, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['class'],
      });
    });
  });
});

Do not forget to replace:

  1. The avatar URL with the URL to your avatar in the 1st line of the code

  2. Badge and Button text with the text you need in the 2nd and 3rd line of the code:

Note: Custom JS only works on the live site after the widget is installed —it won’t run inside the widget editor or preview.


Please try it out and let me know if you like the result :slightly_smiling_face:

Dear Max,
Thank you so much.
I will try it out.