Using the HTML Component to Display Conversations in Retool

The HTML component is a great tool for when you want full control over rendered content and styling. In this post I’ll share how I’m using it within a ListView to render conversations. There are two examples I will walk through:

  • A Gmail-style email thread
  • A Slack-style support conversation

CleanShot 2025-11-24 at 14.09.48

CleanShot 2025-11-24 at 14.10.52

Both examples follow the same pattern:

  1. Fetch conversation data from an API

  2. Use Transform results to turn each item into an HTML string and attach CSS classes.

  3. Display these conversations in my HTML components

Example 1 – Email thread from an API

For the demo, I’m posting dummy email JSON to https://httpbin.org/anything in a query called getEmails.

In getEmails → Transform results:

const emails = data.json.emails || [];

return emails.map(email => {
  const isFromUs = email.fromAddress.endsWith("@yourcompany.com");

  return `
    <div class="email-row ${isFromUs ? 'align-right' : 'align-left'}">
      <div class="email-message ${isFromUs ? 'email-from-us' : 'email-from-customer'}">
        <div class="email-header">
          <div class="email-subject">${email.subject}</div>
          <div class="email-meta">
            <span class="email-from">${email.fromName} &lt;${email.fromAddress}&gt;</span>
            <span class="email-ts">${email.timestamp}</span>
          </div>
        </div>
        <div class="email-body">
          ${email.html_body}
        </div>
      </div>
    </div>
  `;
});

Why transform?

  • To flatten the shape returned by the API into the markup the HTML component needs.

  • To inject CSS classes (email-from-us, email-from-customer, etc) for our styling.

  • To do any additional formatting work here (formatting timestamps, truncating previews)

In my ListView, I set the data source directly to the getEmails query.

Inside the ListView row, the HTML component simply renders the transformed HTML for the current item:

{{ getEmails.data[i] }}

Then in the HTML component’s CSS tab, I style those classes. A few of the things I want to accomplish here are:

  • Customer messages on the left and Support replies on the right

  • Formatted email headers

  • Quoted history indented

.email-row {
  display: flex;
  width: 100%;
  margin-bottom: 10px;
}

.email-row.align-left {
  justify-content: flex-start;
}

.email-row.align-right {
  justify-content: flex-end;
}

.email-message {
  max-width: 85%;
  width: fit-content;
  padding: 12px 16px;
  border-radius: 12px;
  border: 1px solid #dadce0;
  background: #fff;
  font-family: system-ui, sans-serif;
  box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}

.email-from-us {
  background: #e8f0fe;
  border-color: #c6dafc;
}

.email-from-customer {
  background: #fafafa;
}

.email-subject {
  font-weight: 600;
  margin-bottom: 4px;
}

.email-meta {
  font-size: 11px;
  color: #666;
  margin-bottom: 8px;
}

.email-body {
  font-size: 13px;
  line-height: 1.5;
}

.email-body img {
  margin-top: 8px;
  max-width: 100%;
  border-radius: 6px;
}

Example 2 – Slack-style conversation

For Slack, I’m doing exactly the same thing, just with different content and CSS. Again, I post dummy Slack JSON to https://httpbin.org/anything in a query called getSlackMessages.

In getSlackMessages → Transform results:

const msgs = data.json.slackMessages || [];

const EMOJI_MAP = {
  eyes: "👀",
  warning: "⚠️",
  robot_face: "🤖",
  thumbsup: "👍"
};

function timeAgo(iso) {
  const d = new Date(iso);
  const now = new Date();
  const diff = (now - d) / 1000;

  if (diff < 60) return "just now";
  const m = Math.floor(diff / 60);
  if (m < 60) return `${m} mins ago`;
  const h = Math.floor(m / 60);
  if (h < 24) return `${h} hours ago`;
  const days = Math.floor(h / 24);
  return `${days} days ago`;
}

return msgs.map(msg => {
  const isFromUs = msg.userId === "U01SUPPORT" || msg.userId === "U01BOT";

  const reactionsHtml = (msg.reactions || [])
    .map(r => {
      const emoji = EMOJI_MAP[r.name] || `:${r.name}:`;
      return `<span class="slack-reaction-pill">${emoji} ${r.count}</span>`;
    })
    .join("");

  return `
    <div class="slack-row ${isFromUs ? "align-right" : "align-left"}">
      <div class="slack-message ${isFromUs ? "slack-from-us" : "slack-from-customer"}">
        <img class="slack-avatar" src="${msg.avatarUrl}" />
        <div class="slack-main">
          <div class="slack-header">
            <span class="slack-name">${msg.displayName}</span>
            ${msg.isBot ? '<span class="slack-badge">BOT</span>' : ""}
            <span class="slack-ts">${timeAgo(msg.ts)}</span>
          </div>
          <div class="slack-text">
            ${msg.text}
          </div>
          ${
            msg.file
              ? `<div class="slack-file">
                   <img src="${msg.file.url}" alt="${msg.file.name}" />
                 </div>`
              : ""
          }
          ${
            reactionsHtml
              ? `<div class="slack-reactions">${reactionsHtml}</div>`
              : ""
          }
        </div>
      </div>
    </div>
  `;
});

I then populate the ListView and HTML components like I did for the email view. Then in the HTML component’s CSS tab, I style these classes. A few of the things I want to accomplish in this view are:

  • Customer messages on the left and Support + Bot on the right

  • Screenshots inline

  • Emoji reactions rendered as small pills

  • Relative timestamps (6 days ago for example)

.slack-row {
  display: flex;
  width: 100%;
  margin-bottom: 8px;
}
.slack-row.align-left {
  justify-content: flex-start;
}
.slack-row.align-right {
  justify-content: flex-end;
}
.slack-message {
  max-width: 85%;
  width: fit-content; 
  display: flex;
  gap: 10px;
  padding: 12px 16px;
  border-radius: 8px;
  background: #f7f7f7;
  font-family: system-ui, sans-serif;
  box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}

.slack-from-us {
  background: #d4f1ff;
  border: 1px solid #b2e5ff;
}

.slack-from-customer {
  background: #f4f5f7;
}

.slack-avatar {
  width: 36px;
  height: 36px;
  border-radius: 4px;
}
.slack-main {
  flex: 1;
}
.slack-name {
  font-weight: 700;
}
.slack-badge {
  font-size: 10px;
  text-transform: uppercase;
  padding: 1px 4px;
  border-radius: 3px;
  border: 1px solid #c5c5c5;
  color: #616061;
}
.slack-reactions {
  margin-top: 6px;
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}
.slack-reaction-pill {
  font-size: 11px;
  padding: 3px 8px;
  border-radius: 999px;
  background: #fafafa;
  border: 1px solid #ddd;
}
.slack-header {
  margin-bottom: 2px;     
}
.slack-text {
  margin-top: 0;           
  margin-bottom: 4px;      
}
.slack-file {
  margin-top: 0;           
  margin-left: 0;          
}
.slack-file img {
  display: block;          
  max-width: 260px;        
  border-radius: 6px;
  border: 1px solid #ddd;
}
.slack-ts {
  font-size: 11px;
  color: #7c7c7c;
  margin-left: 6px;
  opacity: 0.8;
}

And there you have it! I’ve attached the example app here as well so you can play around with it and make it your own :slight_smile:

HTML Component Messages.json (32.9 KB)

10 Likes