20 octobre 2025

10 conseils pour développer un chatbot via un Web Component

Lors d’une mission récente j’ai récemment dû implémenter un chatbot. C’était une première et je me dis que c’est l’occasion parfaite pour vous partager mon expérience et des conseils si vous aussi passez par là.

Objectif

Usage

Avoir un chatbot facilement utilisable sur n’importe quelle page, léger, performant et personnalisable, selon les besoins du client.

En terme d’usage voici ce que je visais :

<body>
    <chatbot
        toggle-button-text="Fragrance advisor"
        chatbot-header-title-text="Fragrance advisor"
    />
    <script type="module" src="./dist/chatbot.js"></script>
</body>

Stack technique

Web Component via Svelte 5

Mon seul objectif : avoir un chatbot fonctionnel sur n’importe quelle page du site. Je me suis donc dirigé vers un web component. Me restait maintenant à décider comment j’allais créer ce web component. Tout faire nativement ? Utiliser un framework comme Lit ou Svelte ?

J’ai opté pour le combo Svelte 5 + SvelteKit puisque j’avais déjà eu une bonne expérience avec Svelte 4 l’année passée pour un projet personnel.

API

Une API REST et son schema OpenAPI étaient à ma disposition. Je pouvais donc facilement accéder à la session de l’utilisateur, aux conversations, aux messages de la conversation et surtout le endpoint /chat (en server-sent events).

Cet endpoint /chat acceptait uniquement les données dans le body d’une requête en POST. Malheureusement, l’API EventSource qui nous permet d’utiliser les server-sent events dans le navigateur ne prend pas de paramètre dans le body. J’ai donc dû utiliser ce package maintenu par Microsoft : fetch-event-source.

Conseil n°1 : typer ses requêtes automatiquement

Le schema OpenAPI étant disponible, j’ai utilisé openapi-fetch et openapi-typescript pour typer tous mes endpoints et bénéficier de l’autocomplétion Intellisense dans VS Code.

Conseil n°2 : tester ses requêtes dans un environnement agnostique

Je vous recommande Bruno, une alternative open-source à Postman, que j’utilise pour facilement tester mes requêtes ailleurs que dans mon environnement de dev.

Branding & Style

Le style du shadow dom n’est pas totalement isolé

Je voulais fournir un chatbot aussi isolé et autonome que possible. Quand on développe un web component il faut garder en tête que son CSS est presque entièrement isolé du reste de la page. Presque vous dites ? Et oui, il existe une liste d’irréductibles propriétés qui fuitent encore et toujours dans notre Shadow DOM (Source). C’est d’ailleurs une bonne chose puisqu’un web component doit normalement hériter du branding et donc des font-family définies sur la page.

Si on a ce DOM…

<div class="chatbot-wrapper">
    <chatbot />
</div>

… alors une variable CSS --padding définie en global et une propriété color définie sur l’élément parent seront héritées par notre <chatbot> :

/* CSS from the page containing the web component */
:root {
    --padding: 1rem;
}

.chatbot-wrapper {
    color: red;
}

/* CSS of our web component */
:host {
    /* 
        Reset all CSS properties.
        In our case to prevent the
        color of our web component
        to be `red`;
    */
    all: initial;
    padding: var(--padding); /* 1rem */
}

Conseil : ajoutez all: initial au style de reset de votre web component

En ajoutant all: initial aux styles de base de notre Shadow DOM on s’assure de ne pas avoir de mauvaise surprise une fois notre composant en production.

Démonstration de l’utilité de la propriété `all: initial` sur le `:host` de notre composant pour réinitialiser la valeur de certains styles hérités du parent de notre composant.

Détails à prendre en compte pour une bonne UX

Utilisez un textarea

Utilisez un textarea au lieu d’un input pour pouvoir écrire du texte formaté avec des sauts de ligne (Shift + Enter).

<textarea
    bind:this={ui.textarea}
    bind:value={ui.inputValue}
    placeholder={translate(userLanguage).placeholder}
    rows={1}
    disabled={isLoading}
    oninput={autoResize}
    onkeydown={(e) => {
        if (e.key === "Enter" && !e.shiftKey) {
          e.preventDefault();
          onsubmit(e);
        }
    }}
    id="chat-input"
></textarea>

En complément, permettez à la hauteur du textarea de grandir en fonction du contenu:

field-sizing: content;
max-block-size: 7lh;
resize: none;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--grey-light) transparent;

La propriété field-sizing n’est pas encore disponible sur Firefox ni Safari. Vous devrez ajouter ce comportement via JavaScript en complément :

const autoresize = CSS.supports("field-sizing: content") ? null : function (e: Event) {
    const textarea = e.target as HTMLTextAreaElement;
    textarea.style.height = textarea.scrollHeight + "px";
}

Afficher le contenu du message dés que possible

Avec server-sent events, les messages reçus par le endpoint /chat sont divisés en chunk. Au lieu d’attendre d’avoir reçu tous les chunks pour afficher le message final et faire attendre l’utilisateur pour rien, pensez à afficher ces chunks à l’utilisateur dés leur réception pour avoir une impression d’écriture progressive du message par le chatbot.

Aussi, scrollez la zone de chat vers le bas lorsque vous recevez des chunks de l’endpoint /chat pour garder la dernière partie du message toujours visible à l’écran.

Remettre le focus sur le textarea une fois le message reçu

Une fois le message reçu ajoutez le focus sur le textarea pour permettre à l’utilisateur de directement écrire une réponse sans avoir à cliquer.

Il semblerait que la manière la plus efficace à l’heure actuelle est de rajouter un élément dans le bloc parent scrollable contenant nos messages

Avec Svelte, la rune $effect et l’API scroll into view voilà le résultat :

let ui = $state({
  isLoading: false,
  textarea: null as HTMLTextAreaElement | null,
  scrollAnchor: null as HTMLDivElement | null,
});

function scrollToBottom() {
  if (ui.scrollAnchor) {
    requestAnimationFrame(() => {
      ui.scrollAnchor?.scrollIntoView({
        behavior: "smooth",
        block: "end",
      });
    });
  }
}

$effect(() => {
  if (messages.length > 0) {
    // Handle scrolling
    scrollToBottom();

    // Handle focus only when loading completes
    if (!ui.isLoading && ui.textarea) {
      requestAnimationFrame(() => {
        ui.textarea?.focus();
      });
    }
  }
});

Activer/désactiver le scroll sur la page si le chatbot est ouvert

Dans mon cas le chatbot prend toute la hauteur de la page. Donc on doit permettre aux développeurs en charge du site de désactiver/activer le scroll sur la page lorsque le chatbot est ouvert/fermé.

On a certes pas la main sur la page accueillant l’instance de notre web component, mais on peut néanmoins fournir un custom event (CustomEvent API) lorsque notre composant s’ouvre et se ferme via la méthode dispatchEvent.

/**
 * Emit a custom event
 * (c) Chris Ferdinandi, MIT License, https://gomakethings.com
 * @param {String} type - The event type
 * @param {Object} detail - Any details to pass along with the event
 * @see https://gist.github.com/cferdinandi/58d272dd09ef4fe76609b7ae9be89c58#file-index-html-L462-L480
*/
function emit(type: string, detail = {}) {
    let event = new CustomEvent(`chatbot:${type}`, {
        bubbles: true,
        cancelable: true,
        detail: detail,
    });

    const rootElement = document.querySelector("chatbot");
    rootElement?.dispatchEvent(event);
}

function openChatbot() {
    chatbotOpen = true;
    textarea?.focus();
    emit("change-open-state", { open: chatbotOpen });
}

function closeChatbot() {
    chatbotOpen = false;
    emit("change-open-state", { open: chatbotOpen });
}

Permettre à l’utilisateur de retrouver sa conversation après un rechargement de la page

Dans mon cas le chatbot ne fournissait pas de système d’authentification. Mais l’API me fournissait des endpoints pour créer une session, des conversations, et récupérer les messages d’une conversation.

const initChat = async () => {
    const sessionCookie = getCookie("session-id");
    
    if (!sessionCookie) {
      sessionId = `session_${generateUUID()}_${Date.now()}`;
      createSession(sessionId);
    
      const duration = 7 * 24 * 60 * 60;
      const expires = new Date(Date.now() + duration * 1000).toUTCString();
      document.cookie = `session-id=${sessionId}; path=/; expires=${expires}; Secure; SameSite=Strict;`;
    } else {
      sessionId = sessionCookie;
      const session = await getSession(sessionId);
      const conversations = await getConversations(session.id);
      // If we get conversations we retrieve
      // the most recent one and display 
      // its messages in the chat
    }
};

Localisation

Le chatbot devait être disponible dans plusieurs langues. Son contenu est partagé entre le contenu statique et la conversation elle-même.

Contenu statique

Dans mon cas, le contenu statique était composé :

  • des empty states
  • aria-label sur les icons en SVG
  • du <label> du <textarea>
  • d’une politique de confidentialité
  • des conversation starters

Le texte des empty states et de la politique de confidentialité change en fonction de la langue du navigateur de l’utilisateur.

J’ai préconisé un simple objet JavaScript regroupant toutes mes locales et leurs clés respectives pour gérer la traduction de ces contenus.

Voici un exemple simplifié :

export type SupportedLocale = "en" | "fr";
export const supportedLocales: SupportedLocale[] = ["en", "fr"];

const translations: Record<string, Record<string, string>> = {
  en: {
    welcome: "How may I help you?",
  },
  fr: {
    welcome: "Comment puis-je vous aider ?",
  },
};

export function translate(locale: SupportedLocale) {
  const safeLocale = supportedLocales.includes(locale)
    ? (locale as SupportedLocale)
    : "en";

  return translations[safeLocale] as Record<string, string>;
}

Dans mon composant Svelte je pouvais ensuite utiliser la fonction translate en passant la locale du navigateur et la clé souhaitée :

<p class="chatbot-fragrance__welcome-text">
    {translate(userLanguage).welcome}
</p>

Conversation

Le contenu de la conversation étant généré via le backend, je n’avais uniquement qu’à fournir la langue du navigateur au backend dans le endpoint /chat pour définir la langue de la conversation.

En conclusion

Le développement de ce web component via Svelte 5 et SvelteKit s’est déroulé sans encombres. J’aime la simplicité de Svelte et notamment leur documentation très fournie. Je vous conseille d’ailleurs leur MCP fraîchement sorti.

J’espère que ce retour d’expérience et ces quelques conseils vous seront utiles.

Et sur ce je vous dis à tantôt 🖖

Suggestions