Skip to content

Instantly share code, notes, and snippets.

@thinkphp
Created July 27, 2025 15:28
Show Gist options
  • Save thinkphp/c6f861f459ec9457a7a49c28df4fd771 to your computer and use it in GitHub Desktop.
Save thinkphp/c6f861f459ec9457a7a49c28df4fd771 to your computer and use it in GitHub Desktop.
fab web component
class ContactFab extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.isOpen = false;
// Get attributes or set defaults
this.whatsappNumber = this.getAttribute('whatsapp') || '+1234567890';
this.emailAddress = this.getAttribute('email') || '[email protected]';
this.phoneNumber = this.getAttribute('phone') || '+1234567890';
this.instagramUsername = this.getAttribute('instagram') || 'instagram';
this.render();
this.attachEventListeners();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
position: fixed;
bottom: 30px;
right: 30px;
z-index: 1000;
font-family: Arial, sans-serif;
}
.fab-main {
width: 60px;
height: 60px;
background: linear-gradient(45deg, #25D366, #128C7E);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 20px rgba(37, 211, 102, 0.4);
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
border: none;
outline: none;
}
.fab-main:hover {
transform: scale(1.1);
box-shadow: 0 6px 25px rgba(37, 211, 102, 0.6);
}
.fab-main.active {
transform: rotate(45deg);
background: linear-gradient(45deg, #ff4757, #ff3742);
box-shadow: 0 6px 25px rgba(255, 71, 87, 0.6);
}
.fab-icon {
width: 24px;
height: 24px;
fill: white;
transition: transform 0.3s ease;
}
.fab-main.active .fab-icon {
transform: rotate(-45deg);
}
.fab-options {
position: absolute;
bottom: 80px;
right: 0;
display: flex;
flex-direction: column;
gap: 15px;
opacity: 0;
visibility: hidden;
transform: translateY(20px);
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.fab-options.active {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.fab-option {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
color: white;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.fab-option::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(45deg, rgba(255,255,255,0.1), rgba(255,255,255,0.3));
opacity: 0;
transition: opacity 0.3s ease;
}
.fab-option:hover::before {
opacity: 1;
}
.fab-option:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}
.whatsapp {
background: linear-gradient(45deg, #25D366, #128C7E);
animation-delay: 0.1s;
}
.email {
background: linear-gradient(45deg, #EA4335, #D33B2C);
animation-delay: 0.2s;
}
.phone {
background: linear-gradient(45deg, #4285F4, #1a73e8);
animation-delay: 0.3s;
}
.instagram {
background: linear-gradient(45deg, #00BCD4, #0097A7); /* Cyan */
animation-delay: 0.4s;
}
.fab-option-icon {
width: 20px;
height: 20px;
fill: white;
}
.fab-tooltip {
position: absolute;
right: 60px;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 12px;
border-radius: 20px;
font-size: 14px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
pointer-events: none;
}
.fab-tooltip::after {
content: '';
position: absolute;
left: 100%;
top: 50%;
transform: translateY(-50%);
border: 6px solid transparent;
border-left-color: rgba(0, 0, 0, 0.8);
}
.fab-option:hover .fab-tooltip {
opacity: 1;
visibility: visible;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.fab-options.active .fab-option {
animation: bounce 0.6s ease forwards;
}
/* Mobile responsiveness */
@media (max-width: 768px) {
:host {
bottom: 20px;
right: 20px;
}
.fab-main {
width: 55px;
height: 55px;
}
.fab-option {
width: 45px;
height: 45px;
}
.fab-tooltip {
font-size: 12px;
padding: 6px 10px;
}
}
</style>
<div class="contact-fab">
<button class="fab-main" id="fabMain">
<svg class="fab-icon" viewBox="0 0 24 24">
<path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
</svg>
</button>
<div class="fab-options" id="fabOptions">
<a href="https://wa.me/${this.whatsappNumber.replace(/\D/g, '')}" class="fab-option whatsapp" target="_blank">
<svg class="fab-option-icon" viewBox="0 0 24 24">
<path d="M17.472,14.382c-0.297-0.149-1.758-0.867-2.03-0.967c-0.273-0.099-0.471-0.148-0.67,0.15c-0.197,0.297-0.767,0.966-0.94,1.164c-0.173,0.199-0.347,0.223-0.644,0.075c-0.297-0.15-1.255-0.463-2.39-1.475c-0.883-0.788-1.48-1.761-1.653-2.059c-0.173-0.297-0.018-0.458,0.13-0.606c0.134-0.133,0.297-0.347,0.446-0.52C9.87,9.97,9.919,9.846,10.019,9.65c0.099-0.198,0.05-0.371-0.025-0.52C9.919,8.981,9.325,7.515,9.078,6.92c-0.241-0.58-0.487-0.5-0.669-0.51c-0.173-0.008-0.371-0.01-0.57-0.01c-0.198,0-0.52,0.074-0.792,0.372c-0.272,0.297-1.04,1.016-1.04,2.479c0,1.462,1.065,2.875,1.213,3.074c0.149,0.198,2.096,3.2,5.077,4.487c0.709,0.306,1.262,0.489,1.694,0.625c0.712,0.227,1.36,0.195,1.871,0.118c0.571-0.085,1.758-0.719,2.006-1.413c0.248-0.694,0.248-1.289,0.173-1.413C17.944,14.653,17.769,14.531,17.472,14.382z M12.057,21.785h-0.008c-1.805,0-3.573-0.488-5.126-1.411l-0.368-0.218l-3.815,1.002l1.018-3.721l-0.238-0.382C2.438,15.25,1.898,13.688,1.898,12.057c0-5.561,4.526-10.086,10.091-10.086c2.693,0,5.225,1.05,7.127,2.953c1.902,1.903,2.951,4.435,2.951,7.128C22.067,17.613,17.541,22.14,12.057,21.785z"/>
</svg>
<div class="fab-tooltip">WhatsApp</div>
</a>
<a href="mailto:${this.emailAddress}" class="fab-option email">
<svg class="fab-option-icon" viewBox="0 0 24 24">
<path d="M20,8L12,13L4,8V6L12,11L20,6M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z" />
</svg>
<div class="fab-tooltip">Email Us</div>
</a>
<a href="tel:${this.phoneNumber}" class="fab-option phone">
<svg class="fab-option-icon" viewBox="0 0 24 24">
<path d="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z" />
</svg>
<div class="fab-tooltip">Call Us</div>
</a>
<a href="https://instagram.com/${this.instagramUsername}" class="fab-option instagram" target="_blank">
<svg class="fab-option-icon" viewBox="0 0 24 24">
<path d="M7.8,2H16.2C19.4,2 22,4.6 22,7.8V16.2A5.8,5.8 0 0,1 16.2,22H7.8C4.6,22 2,19.4 2,16.2V7.8A5.8,5.8 0 0,1 7.8,2M7.6,4A3.6,3.6 0 0,0 4,7.6V16.4C4,18.39 5.61,20 7.6,20H16.4A3.6,3.6 0 0,0 20,16.4V7.6C20,5.61 18.39,4 16.4,4H7.6M17.25,5.5A1.25,1.25 0 0,1 18.5,6.75A1.25,1.25 0 0,1 17.25,8A1.25,1.25 0 0,1 16,6.75A1.25,1.25 0 0,1 17.25,5.5M12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9Z" />
</svg>
<div class="fab-tooltip">Instagram</div>
</a>
</div>
</div>
`;
}
attachEventListeners() {
const fabMain = this.shadowRoot.getElementById('fabMain');
const fabOptions = this.shadowRoot.getElementById('fabOptions');
fabMain.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleFab();
});
// Close FAB when clicking outside
document.addEventListener('click', (e) => {
if (!e.composedPath().includes(this)) {
this.closeFab();
}
});
}
toggleFab() {
this.isOpen = !this.isOpen;
const fabMain = this.shadowRoot.getElementById('fabMain');
const fabOptions = this.shadowRoot.getElementById('fabOptions');
if (this.isOpen) {
fabMain.classList.add('active');
fabOptions.classList.add('active');
} else {
fabMain.classList.remove('active');
fabOptions.classList.remove('active');
}
}
closeFab() {
if (this.isOpen) {
this.isOpen = false;
const fabMain = this.shadowRoot.getElementById('fabMain');
const fabOptions = this.shadowRoot.getElementById('fabOptions');
fabMain.classList.remove('active');
fabOptions.classList.remove('active');
}
}
static get observedAttributes() {
return ['whatsapp', 'email', 'phone', 'instagram'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
if (name === 'whatsapp') {
this.whatsappNumber = newValue;
} else if (name === 'email') {
this.emailAddress = newValue;
} else if (name === 'phone') {
this.phoneNumber = newValue;
} else if (name === 'instagram') {
this.instagramUsername = newValue;
}
this.render();
this.attachEventListeners();
}
}
}
customElements.define('contact-fab', ContactFab);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment