A chat-room widget: scrollable history of messages above, a markdown input + Send button below. The input is a <k-markdown-editor> — users type markdown (with a Preview tab to see the rendered output before sending), and the message is rendered to HTML via the vendored marked parser (CommonMark + GFM, including tables and task lists). The rendered HTML runs through sanitizeHtml on its way into the message bubble so any inline HTML the user types is filtered against the allow-list — <script>, <iframe>, etc. are always stripped.
The component fires a send event when the user submits. Listen for it and call chat.addMessage(...) for incoming replies. Outgoing messages are added automatically.
<k-chat id="basicChat"></k-chat>
<script>
const chat = document.getElementById('basicChat');
chat.addEventListener('send', (e) => {
console.log('Sent:', e.detail.markdown);
setTimeout(() => {
chat.addMessage({
type: 'incoming',
html: 'Got it!',
sender: 'Bot'
});
}, 500);
});
</script>
By default the chat matches Slack/Discord/Messages: Enter sends, Shift+Enter inserts a newline. Add the enter-newline attribute to flip it — Enter inserts a newline, Shift+Enter sends.
<k-chat enter-newline></k-chat>
Add show-status and outgoing messages get an iMessage-style status indicator below the bubble. The four statuses are sending, delivered, read, and failed. When the user submits, the message is added with status: 'sending'; advance it via chat.updateMessage(id, { status }) as the backend confirms.
The attribute takes one of two display modes:
show-status or show-status="text" — small grey text labels (Sending…, Delivered, Read, Not Delivered). Default mode.show-status="icons" — spinner / check / double-check / error icons only.Following iMessage convention, the indicator only renders on the most recent outgoing message — older delivered/read messages stay clean. sending and failed always show regardless of position so the user knows about in-flight or stuck messages immediately.
<k-chat id="ackChat" show-status></k-chat>
<script type="module">
await customElements.whenDefined('k-chat');
const chat = document.getElementById('ackChat');
chat.addEventListener('send', async (e) => {
// Simulate a server round-trip
await new Promise(r => setTimeout(r, 800));
chat.updateMessage(e.detail.id, {
status: 'delivered'
});
await new Promise(r => setTimeout(r, 1200));
chat.updateMessage(e.detail.id, {
status: 'read'
});
});
</script>
Pass show-status="icons" for compact icon-only badges (spinner / single check / double check / red error) instead of text labels — closer to WhatsApp / Telegram styling.
<k-chat id="iconChat" show-status="icons"></k-chat>
<script type="module">
await customElements.whenDefined('k-chat');
const chat = document.getElementById('iconChat');
chat.addEventListener('send', async (e) => {
await new Promise(r => setTimeout(r, 800));
chat.updateMessage(e.detail.id, {
status: 'delivered'
});
await new Promise(r => setTimeout(r, 1200));
chat.updateMessage(e.detail.id, {
status: 'read'
});
});
</script>
The disabled attribute prevents user interaction. Useful while waiting for connection or rate-limiting.
<k-chat disabled></k-chat>
new Chat()enterNewline: BooleanInverts the default Enter-key behavior. Default is false (Slack-style: Enter sends, Shift+Enter inserts a newline). When true: Enter inserts a newline and Shift+Enter sends. Syncs to enter-newline attribute.
showStatus: StringEnables and configures the per-message status indicator. Possible values: null/empty (off, default), "text" (small grey labels — the default mode when the attribute is set without a value), or "icons" (spinner/check/double-check/error glyphs). The indicator only appears on the most recent outgoing message, except for sending and failed which always show. Outgoing messages submitted via the input start as sending — advance them via updateMessage(id, { status }) as the backend confirms. Syncs to show-status attribute.
placeholder: StringPlaceholder text for the input. Default "Type a message...". Syncs to placeholder attribute.
disabled: BooleanWhen true, the input and Send button are disabled. Syncs to disabled attribute.
messages: Array (read-only state)The current array of messages. Each entry is { id, type, html, status, sender, timestamp }. Don't mutate directly — use the public methods.
addMessage(msg): stringAppend a message and return its id. msg object accepts { id, type, html, status, sender, timestamp }; missing fields are filled in (auto-generated id, type defaults to "incoming", etc.). HTML is sanitized.
updateMessage(id, updates): booleanUpdate an existing message's html, status, sender, or timestamp. Returns true if a message with that id was found.
removeMessage(id): booleanRemove a message. Returns true if found.
clear(): voidRemove every message.
send(): string | nullProgrammatically submit whatever is currently in the input. Returns the new outgoing message's id, or null if the input was empty.
| Variable | Default | Description |
|---|---|---|
--window_min_height | 16rem | Minimum height of the message window (the chat won't collapse below this even when empty) |
--window_max_height | 32rem | Maximum height of the message window before it scrolls internally |
--chat_padding | 0.5rem | Padding inside both the message window and the input row |
--chat_gap | 0.5rem | Gap between messages and gap between the editor and the Send button |
--height | 13rem | Initial height of the embedded markdown editor; the user can drag the resize handle below it to grow or shrink past this |
--message_max_width | 75% | Max width of a single message bubble |
--message_radius | 1rem | Border radius of message bubbles |
--bubble_padding | 0.5rem 0.75rem | Padding inside bubbles |
--bubble_bg__incoming | var(--c_bg__alt) | Background color of incoming bubbles |
--bubble_bg__outgoing | var(--c_primary) | Background color of outgoing bubbles |
--bubble_tc__outgoing | white | Text color of outgoing bubbles |
sendFires when the user submits. detail contains { id, html, markdown } — html is the rendered+sanitized output (the same string used in the bubble) and markdown is the raw source the user typed. The outgoing message is already in the conversation by the time the event fires — if you need to roll it back (e.g. on a network failure), call updateMessage(id, { status: 'failed' }) or removeMessage(id).
Every message that enters the chat (whether from the user's editor, from addMessage(), or from updateMessage()) runs through sanitizeHtml before it is added to the DOM. That strips <script>, <style>, <iframe>, event-handler attributes, and unsafe URL schemes — but it only protects content that flows through this component.
If you persist messages on a server and rebroadcast them to other users, the server must independently sanitize before storing and again before sending. A determined attacker can submit raw HTTP without ever loading your client code.