Suggestion for an Avatar Badge Custom Module

This is the code produced by Wappler's Sematic Patterns for this avatar badge

image

<span class="rounded-circle bg-primary text-white d-inline-flex align-items-center justify-content-center fw-semibold border border-2 border-white" style="width:32px;height:32px;">AV</span>

This is the code produced for this avatar badge

image

<img wappler-role="avatar" class="rounded-circle object-fit-cover ms-5" width="48" height="48" src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=128&amp;h=128&amp;fit=crop&amp;auto=format" alt="Avatar" loading="lazy">

What if we simplified this by using a custom element for the first avatar:

<avatar-badge name="Alice Vance" size="32"></avatar-badge>

or

<avatar-badge initials="AV" size="32"></avatar-badge>

Or for the image avatar

<avatar-badge src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=128&amp;h=128&amp;fit=crop&amp;auto=format" name="Alice Vance" size="48" ></avatar-badge>

Or to show that the person is online

image

<avatar-badge src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=128&amp;h=128&amp;fit=crop&amp;auto=format" name="Alice Vance" size="48" status="online"></avatar-badge>

All that is required is a JS file with the following content:

class AvatarBadge extends HTMLElement {
    static get observedAttributes() {
        return ["src", "name", "initials", "size", "bg", "text", "border", "shape", "status"];
    }

    connectedCallback() {
        this.render();
    }

    attributeChangedCallback() {
        this.render();
    }

    getInitials() {
        if (this.getAttribute("initials")) {
        return this.getAttribute("initials");
        }

        const name = this.getAttribute("name");
        if (!name) return "";

        const parts = name.trim().split(/\s+/);
        if (parts.length === 1) return parts[0][0].toUpperCase();

        return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
    }

    render() {
        const src = this.getAttribute("src");
        const initials = this.getInitials();
        const size = parseInt(this.getAttribute("size"), 10) || 40;

        const bg = this.getAttribute("bg") || "primary";
        const text = this.getAttribute("text") || "white";
        const borderAttr = this.getAttribute("border");
        const hasBorder = borderAttr !== null;
        const border = (borderAttr && borderAttr.trim()) ? borderAttr.trim() : "white";
        const borderClass = hasBorder ? `border border-2 border-${border}` : "";
        const shape = this.getAttribute("shape") || "circle"; // circle | rounded | square

        const status = (this.getAttribute("status") || "").toLowerCase();
        const showStatus = !!status;
        const statusColor =
            status === "online" ? "success" :
            status === "offline" ? "secondary" :
            status === "away" ? "warning" :
            status === "busy" ? "danger" :
            status;
        const dotSize = Math.max(10, Math.round(size * 0.28));
        const dotHtml = showStatus ? `
            <span
            class="position-absolute rounded-circle bg-${statusColor} ${borderClass}"
            style="width:${dotSize}px;height:${dotSize}px;right:-1px;bottom:-1px;"
            aria-hidden="true"
            ></span>
        ` : "";

        const shapeClass =
        shape === "square"
            ? ""
            : shape === "rounded"
            ? "rounded"
            : "rounded-circle";

        const aria = this.getAttribute("name") || initials || "avatar";

        if (src) {
    this.innerHTML = `
    <div style="position:relative; width:${size}px; height:${size}px;">
        <img
        src="${src}"
        alt="${aria}"
        loading="lazy"
        class="${borderClass} ${shapeClass}"
        style="position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block;"
        onload="this.nextElementSibling.style.display='none';"
        onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';"
        >
        <span
        class="align-items-center justify-content-center fw-semibold bg-${bg} text-${text} ${borderClass} ${shapeClass}"
        style="position:absolute; inset:0; width:100%; height:100%; display:none; align-items:center; justify-content:center;"
        aria-label="${aria}"
        >${initials}</span>
        ${dotHtml}
    </div>
    `;
        } else {
        this.innerHTML = `
            <div style="position:relative; width:${size}px; height:${size}px;">
                <span
                class="d-flex align-items-center justify-content-center fw-semibold bg-${bg} text-${text} ${borderClass} ${shapeClass}"
                style="position:absolute; inset:0; width:100%; height:100%;"
                aria-label="${aria}"
                >${initials}</span>
                ${dotHtml}
            </div>
        `;
        }
    }
}

if (!customElements.get("avatar-badge")) {
    customElements.define("avatar-badge", AvatarBadge);
}

<avatar-badge> supports these attributes (all optional):

  • src
    • Image URL/path. If omitted, it renders initials only.
  • name
    • Used to compute initials (first + last letter). Also used for alt text.
  • initials
    • Explicit initials override. If set, it takes precedence over name.
  • size
    • Avatar size in pixels (number). Default: 40.
  • shape
    • circle (default), rounded, square
  • bg
    • Bootstrap background color name for the initials fallback (default: primary)
    • Examples: primary, secondary, success, danger, warning, info, dark, light
  • text
    • Bootstrap text color name for initials (default: white)
    • Examples: white, dark, light, etc.
  • border
    • Border is off by default.
    • If the border attribute is present, border is enabled.
    • Value is the Bootstrap border color (default when present but empty: white).
    • Example: border="white" or border="dark"
  • status
    • Adds a bottom-right status dot when set.
    • Supported keywords:
      • online → success
      • offline → secondary
      • away → warning
      • busy → danger
    • Or pass a Bootstrap color directly (e.g. status="info").

If there is enough animo for this, I could create a custom extension, but I prefer that the Wappler team included this and like custom elements as part of Wappler

Such a pity that there is not enough interest in this subject.

I have sent @Hyperbytes (Brian) an PM with a link to the extension. Please enjoy.

1 Like

Hi Ben,

I’m very glad you follow up on the semantic bootstrap components. IMHO they represent a significant step forward in quick building bootstrap layouts.

When making them we did think on the crossroad which way should we take, the pure html way with custom html structure and properties to it or making a complete app connect components from each semantic element.

To for now we chose the custom html route to allow rich customization by the user, and as having the fastest rendering, but in the future we can also create custom app connect components as well.

Having such components will definitely also provide many advantages like shorter html by using custom tags and cleaner html structure.

Thank you @George for your reply, I look forward to Wappler's implementation, be it buit-in or as a facility to create our own components.

Web Components allow HTML to grow to new superpowers.

Some of the Components already publised:
@benpley/wappler-responsive-sidebar - npm
@benpley/wappler-theme-changer - npm - superseded by Wappler's Theme Manager
wappler-youtube-player - npm
and, yes ...
@benpley/wappler-avatar-badge - npm

1 Like

Good job Ben!

You have an interesting combination of web components that are also wrapped and available as app connect components - neat idea!

Not sure how the performance will be and the rendering time, will have to do some tests - what is your experience?

Maybe also @patrick can check it out and give some advise of the best combinations