Pepita Support

Pepita Support — widget

One script tag drops a chat button and a chat window onto any page. The button opens and closes the widget; unread replies raise a single, styleable signal.

Quick start

Paste this once, before </body>. Use the API key from your space settings.

<script
  src="https://widget.pepita.support/widget.js"
  data-key="pk_live_xxxxxxxxxxxxxxxxxxxxxxxx"
  defer></script>

That is the whole install: a floating button appears bottom-right, and clicking it opens the chat. Everything else below is optional — placement, styling, and control.

Attributes

AttributePurpose
data-key requiredYour space's public API key. Also gates which origins may embed.
data-parent-chatCSS selector of an element to mount the chat window into, instead of floating.
data-parent-triggerCSS selector of an element to mount the button into, instead of floating bottom-right.
data-soundSet to "off" to silence the reply alert. See The alert sound.

The unread badge

When there are replies the visitor hasn't seen, the widget sets a data-pending attribute on the button. Style it however you like — the dot's look is yours; the signal is ours:

/* a red dot in the top-right corner of the button */
.pepita-trigger[data-pending]::after {
  content: "";
  position: absolute; top: -1px; right: -1px;
  width: 14px; height: 14px; border-radius: 50%;
  background: #d9544a; border: 2px solid var(--your-bg);
}
Forward-compatible. Match on the attribute's presence ([data-pending]), not a value. A future release may carry a count (data-pending="3"); existence-based CSS keeps working unchanged, and you can opt into the number with content: attr(data-pending) whenever you want it.

The button is position: relative in the default skin so the absolutely-positioned dot anchors to it. If you bring your own skin, keep that.

The “new messages” pill

While the panel is open, the transcript sticks to the newest message. If the visitor scrolls up to re-read history and a reply arrives, the widget won't yank them down — instead it reveals a small pill at the bottom of the transcript by setting data-visible on .pepita-newmsg. Clicking the pill jumps to the latest message. As with the badge, the signal is ours; the look is yours:

.pepita-newmsg[data-visible] {
  /* your pill — the widget only toggles data-visible */
  background: #286983; color: #fff;
  border-radius: 999px; padding: 4px 12px;
}
Presence, not value. Match on [data-visible]'s presence, just like the badge. You only need to paint the pill — the widget sets its position (and the surrounding .pepita-messages-wrap layout) inline, so the transcript stays scrollable and the pill stays pinned even under a fully custom skin.

The alert sound

When an operator replies and the visitor isn't watching — panel closed, tab hidden, or the window unfocused — the widget plays a short chime (widget-alert.mp3, served from the widget's own origin). It never sounds for a reply the visitor is already looking at. To silence it:

<script
  src="https://widget.pepita.support/widget.js"
  data-key="pk_live_xxxxxxxxxxxxxxxxxxxxxxxx"
  data-sound="off"
  defer></script>
Silence is the only thing you turn off. The data-pending badge still appears — muting removes the noise, not the unread signal. Note that browsers block autoplay until the visitor has interacted with your page, so the first alert of a session may be swallowed by the browser even with sound on; the badge is the signal you can rely on.

Operator replies are markdown

A reply from your support operator is rendered as markdown — **bold**, *italic*, `code`, links, bullet and numbered lists, fenced code blocks and # headings. A single newline becomes a line break. The resulting elements are ordinary children of .pepita-message--admin, so style them like any other content:

.pepita-message--admin p { /* … */ }
.pepita-message--admin ul,
.pepita-message--admin ol { /* … */ }
.pepita-message--admin a { /* … */ }
.pepita-message--admin code,
.pepita-message--admin pre { /* … */ }
.pepita-message--admin h1,
.pepita-message--admin h2,
.pepita-message--admin h3 { /* keep these near body size — a document-scale
                                heading swallows a 300px panel */ }
The visitor's own messages are never markdown. They are rendered as plain text, always — visitor input is never treated as markup. Operator replies are parsed and then sanitized (raw HTML an operator types shows up as literal characters, and <script>, event handlers and inline style are stripped), so nothing executable reaches your page. If you bring your own skin, the barebone stylesheet is worth reading as a starting point for the rules above.

The greeting in an empty chat

Until the first message, the transcript shows the space's greeting (set in its widget settings) in .pepita-empty — a child of .pepita-messages, deliberately not a message bubble, because it isn't from a person. It is markdown, rendered and sanitized exactly like an operator's reply, so it can hold paragraphs, lists, links and code. It is removed as soon as the chat has a message.

.pepita-empty { /* the greeting box itself */ }
.pepita-empty p,
.pepita-empty ul,
.pepita-empty a { /* same children as an operator reply */ }
A space with no greeting configured renders nothing at all — the element is absent rather than empty, so a rule with padding or a border can't leave a stray box behind in a fresh chat.

Opening hours and the away notice

A space can keep opening hours. Outside them the widget shows a one-line notice between the transcript and the composer — .pepita-away, carrying data-visible whenever it has something to say. The composer stays usable the whole time: a visitor can always leave a message, and the wording tells them so.

.pepita-away {
  /* your notice — the widget only toggles data-visible */
  flex: 0 0 auto;  /* a flex item of a fixed-height column — see below */
  border-top: 1px solid #e5dfd3; padding: 6px 10px;
  font-size: 12.5px; color: #6e6a86;
}
.pepita-away[data-visible] { animation: fade-in 0.16s ease-out; }
Don't set display on it. The widget shows and hides the strip with an inline style, so a stylesheet display is overridden — including the display: flex you might reach for to lay the text out. Match on [data-visible]'s presence, as with the pill and the badge. The reopening time is worked out on the visitor's own clock, so “back tomorrow at 9am” reads in their timezone rather than yours, and the text is rewritten in place instead of appended — it can never say the same thing twice.
Two sizing details worth copying. flex: 0 0 auto, because the notice is a flex item of a fixed-height column: without it a two-line notice is squeezed instead of taking its space from the transcript. And an absolute font-size if your message rules use one — .pepita-window sets no font size, so an em here resolves against whatever your page inherits, and the notice can come out larger than the messages it sits under. (The barebone skin does use em — there the messages are relative too, so the two stay in proportion.)

Styling

The widget renders stable, unstyled class names — bring your own CSS, or load the barebone skin:

<link rel="stylesheet" href="https://widget.pepita.support/widget.css">
ClassElement
.pepita-rootWrapper the widget appends to <body> when it places itself. It also gets .pepita--floating — see below.
.pepita-triggerThe button that opens/closes the chat (carries [data-pending]).
.pepita-windowThe chat panel; the widget toggles .pepita-window--open when it's shown.
.pepita-header / .pepita-titlePanel header and its title.
.pepita-minimizeThe button at the right of the header. It closes the panel; the trigger reopens it.
.pepita-messagesThe scrolling transcript.
.pepita-messages-wrapPositioned wrapper around the transcript; anchors the new-message pill.
.pepita-newmsgThe “new messages” pill (carries [data-visible]); click jumps to the latest.
.pepita-message--visitor / --adminA single message bubble, by sender. An operator's reply contains rendered markdown children (p, ul, code, …). A visitor's bubble is plain text, and the widget sets white-space: pre-wrap on it inline so their line breaks survive your stylesheet.
.pepita-emptyThe greeting shown while the transcript is empty. Markdown children, like an operator's reply; absent entirely when the space configures no greeting.
.pepita-awayThe opening-hours notice between the transcript and the composer (carries [data-visible]). The widget owns its display — see above.
.pepita-inputrow / .pepita-input / .pepita-sendThe composer. .pepita-input is a <textarea> that grows with its content up to 5 rows, then scrolls (Enter sends, Shift+Enter adds a line).

Floating vs. mounted

With no data-parent-chat, the widget places itself: it appends .pepita-root to <body> and adds .pepita--floating to it. That class is where all corner positioning lives — if you write your own stylesheet and skip it, the button and panel land in the page flow instead of the corner. The barebone skin does this:

.pepita--floating { position: fixed; right: 20px; bottom: 20px; z-index: 2147483000; }
.pepita--floating .pepita-window { position: fixed; right: 20px; bottom: 84px; }

Point data-parent-chat / data-parent-trigger at your own elements and the widget appends the panel / button straight into them. Your containers then own the placement: no .pepita-root is created, no .pepita--floating is set, and the rules above are inert.

Set them in pairs. If you supply data-parent-chat but no data-parent-trigger, the panel goes into your container while the button is appended to the end of <body> with no positioning class on it — it will sit in the page flow, not in a corner. Either give the button a container too, or position .pepita-trigger yourself.

Controlling it from your code

The widget exposes a small global once it boots. Use it to wire your own launcher (e.g. a header button) to the same widget:

// the whole surface: open / close / toggle the panel
window.PepitaChat.open();
window.PepitaChat.close();
window.PepitaChat.toggle();
That's the whole API. The chat button and the data-pending badge are the one, consistent way to integrate — that consistency is the point. There are no event hooks to subscribe to: unread state lives on the DOM (data-pending), not in JavaScript. Use open()/toggle() only to drive the panel from your own element (e.g. a "Contact us" link).

Good to know

  • Evergreen, and it never reloads your page. No version in the URL — you never re-embed. When a long-open tab is running an old build, the widget swaps its own bundle in place: it tears down its DOM, listeners and socket, then boots the new one. Your page keeps its state — nothing the visitor was doing is thrown away for a widget update.
  • One visitor across sub-domains. A conversation started on example.com continues on app.example.com: the visitor id lives in a first-party cookie scoped to your registrable domain.
  • Origins are gated. A key only works from the domains registered for that space.