floors.js Documentation

Configure everything from your dashboard.

Customize your widget

Configure everything easily from your dashboard.

Setup
data-key
Required
Your site identifier. Scopes all visitors and rooms to your site.
data-key="flr_..."
data-name
default: page <title>
Display name shown in the widget header.
data-name="My Product"
data-open
default: "false"
Open the widget panel automatically on page load.
data-open="true"
data-owner-always
default: "false"
Always show the owner's avatar in the home room, even when the owner is offline or in another room.
data-owner-always="true"
Rooms
data-pages
default: auto-detect
Comma-separated list of pages that show a visible door in the 3D scene. The widget works on every page regardless — this controls which rooms have a door to keep the scene clean. Auto-detected from your <nav> if omitted. Use as to rename: /ugly-path as Pretty Name. Up to 10 rooms.
data-pages="/,/about,/pricing"
data-exclude
default: none
Comma-separated paths to hide the widget on. Supports wildcards (*). Widget disconnects on these pages.
data-exclude="/login,/signup,/app/*"
Appearance
data-accent
default: "#6366f1"
Custom accent color for the widget UI. Match it to your brand.
data-accent="#FF6B00"
data-theme
default: "dark"
Default color theme for the widget. Visitors who toggle the theme manually keep their preference via localStorage.
data-theme="light"
data-theme-toggle
default: "false"
Show a sun/moon toggle in the widget header so visitors can switch between light and dark mode.
data-theme-toggle="true"
Chat
data-pinned-message
default: none
A message pinned at the top of the chat. Use a JSON map for per-room messages.
data-pinned-message="Hey, ask me anything!"
data-pinned-message='{"*":"Welcome!","/pricing":"Check our plans!"}'
data-room-chat
default: "false"
Scope chat messages to the current room. When enabled, visitors only see messages sent from the same page. By default, chat is shared across all rooms.
data-room-chat="true"
data-no-links
default: "true"
Block messages containing URLs or links in the chat. Prevents spam and self-promotion. Set to "false" to allow links.
data-no-links="false"
data-no-promo
default: "true"
Block promotional messages (buy, discount, promo, subscribe, follow me…) from the chat. Set to "false" to allow promo.
data-no-promo="false"
data-auto-reply-keyword
data-auto-reply-message
default: none
Auto-reply whisper triggered when a visitor types an exact keyword (case-sensitive). Only the visitor who typed it sees the reply. Use both attributes together.
data-auto-reply-keyword="DISCOUNT"
data-auto-reply-message="Here's your code: CHAT25"
Visitors
data-allow-rename
default: "false"
Allow visitors to change their display name after entering. Disabled by default — set to "true" to let visitors rename themselves.
data-allow-rename="true"
data-private
default: "false"
Private mode — visitors are isolated from each other. Each visitor only sees the owner. Visitor messages are only visible to the owner. Owner messages are broadcast to all visitors. Ideal for support or helpdesk use cases.
data-private="true"
Users only
data-user-token
default: none
A short-lived token your backend signs for an authenticated user. When "Users only" is enabled in your dashboard, only visitors carrying a valid token can join — everyone else sees a locked panel. The token is an HMAC of { uid, name, exp } signed with your Widget secret (Dashboard → Settings). Sign it on your backend only — never expose the secret to the browser. SPA alternative: call window.Floors.identify(token) after the user logs in.
data-user-token="<token from your backend>"

🔒 "Users only" — backend setup

Enable Users only in your Dashboard and copy your Widget secret. Then, on pages rendered for a logged-in user, have your backend sign a token and pass it as data-user-token (or via window.Floors.identify(token)). floors.js never touches your user database — it only verifies the signature.

Token format (all languages)
payload = base64url(JSON({ uid, name, exp: nowMs + 3600000 }))
sig     = base64url( HMAC_SHA256(payload, WIDGET_SECRET) )
token   = payload + "." + sig
Next.js (App Router)
// app/lib/floors.ts
import crypto from "crypto";
const b64url = (b) => Buffer.from(b).toString("base64url");

export function floorsToken(user) {
  const payload = b64url(JSON.stringify({
    uid: user.id, name: user.name, exp: Date.now() + 3600_000,
  }));
  const sig = crypto.createHmac("sha256", process.env.FLOORS_WIDGET_SECRET)
    .update(payload).digest("base64url");
  return payload + "." + sig;
}

// app/(app)/layout.tsx — server component
import { floorsToken } from "@/app/lib/floors";
import { getSession } from "@/app/lib/auth"; // your auth

export default async function AppLayout({ children }) {
  const session = await getSession();
  const token = session ? floorsToken(session.user) : null;
  return (
    <>
      {children}
      <script
        src="https://floorsjs.com/embed/floors.min.js"
        data-key="flr_..."
        {...(token ? { "data-user-token": token } : {})}
        async
      />
    </>
  );
}
Next.js (Pages Router) / React SPA
// pages/api/floors-token.ts — returns a token for the logged-in user
import crypto from "crypto";
import { getSession } from "@/lib/auth";
const b64url = (b) => Buffer.from(b).toString("base64url");

export default async function handler(req, res) {
  const session = await getSession(req);
  if (!session) return res.status(401).end();
  const payload = b64url(JSON.stringify({
    uid: session.user.id, name: session.user.name, exp: Date.now() + 3600_000,
  }));
  const sig = crypto.createHmac("sha256", process.env.FLOORS_WIDGET_SECRET)
    .update(payload).digest("base64url");
  res.json({ token: payload + "." + sig });
}

// client, after login:
const { token } = await fetch("/api/floors-token").then(r => r.json());
window.Floors?.identify(token);
Node.js / Express
import crypto from "crypto";
const b64url = (b) => Buffer.from(b).toString("base64url");
const SECRET = process.env.FLOORS_WIDGET_SECRET;

app.get("/app", (req, res) => {
  let tag = '<script src="https://floorsjs.com/embed/floors.min.js" data-key="flr_..." async></scr'+'ipt>';
  if (req.user) {
    const payload = b64url(JSON.stringify({
      uid: req.user.id, name: req.user.name, exp: Date.now() + 3600_000,
    }));
    const sig = crypto.createHmac("sha256", SECRET).update(payload).digest("base64url");
    const token = payload + "." + sig;
    tag = '<script src="https://floorsjs.com/embed/floors.min.js" data-key="flr_..." data-user-token="' + token + '" async></scr'+'ipt>';
  }
  res.send(renderPage({ floorsTag: tag }));
});
PHP / Laravel
function floors_token($user) {
  $secret  = env('FLOORS_WIDGET_SECRET');
  $payload = rtrim(strtr(base64_encode(json_encode([
    'uid' => $user->id, 'name' => $user->name,
    'exp' => (int)(microtime(true) * 1000) + 3600000,
  ])), '+/', '-_'), '=');
  $sig = rtrim(strtr(base64_encode(
    hash_hmac('sha256', $payload, $secret, true)
  ), '+/', '-_'), '=');
  return $payload . '.' . $sig;
}

// Blade (logged-in only):
// @auth <script src="https://floorsjs.com/embed/floors.min.js"
//   data-key="flr_..." data-user-token="{{ floors_token(auth()->user()) }}"></script> @endauth
Python / Django / Flask
import os, json, time, hmac, hashlib, base64

def b64url(b: bytes) -> str:
    return base64.urlsafe_b64encode(b).rstrip(b"=").decode()

def floors_token(user) -> str:
    secret = os.environ["FLOORS_WIDGET_SECRET"].encode()
    payload = b64url(json.dumps({
        "uid": str(user.id), "name": user.name,
        "exp": int(time.time() * 1000) + 3_600_000,
    }).encode())
    sig = b64url(hmac.new(secret, payload.encode(), hashlib.sha256).digest())
    return f"{payload}.{sig}"

# template context: token = floors_token(request.user) if request.user.is_authenticated else None
Ruby on Rails
require "openssl"; require "base64"; require "json"

def floors_token(user)
  secret  = ENV["FLOORS_WIDGET_SECRET"]
  payload = Base64.urlsafe_encode64(
    { uid: user.id, name: user.name, exp: (Time.now.to_f * 1000).to_i + 3_600_000 }.to_json,
    padding: false
  )
  sig = Base64.urlsafe_encode64(
    OpenSSL::HMAC.digest("SHA256", secret, payload), padding: false
  )
  "#{payload}.#{sig}"
end

# erb (logged-in only):
# <% if user_signed_in? %><script src="https://floorsjs.com/embed/floors.min.js"
#   data-key="flr_..." data-user-token="<%= floors_token(current_user) %>"></script><% end %>
Anonymous visitors get no token → the widget shows a locked panel and cannot join. Logged-in users get a signed token → access granted, and their name from the token is used. Keep exp short (≈1h); refresh by re-rendering the page or calling window.Floors.identify(newToken). Rotating the Widget secret invalidates all existing tokens.

Discord notifications

Get pinged on Discord when a visitor sends a message on your site.

1

After your payment, you'll receive an email with a private link to your Dashboard.

2

Click the link, go to Settings, paste your Discord webhook URL, and hit Save. Done.

Your webhook URL never leaves our server. Visitors and anyone viewing your site's source code cannot see it. Notifications are rate-limited to one per minute per site.
Telegram

Telegram bot

Reply to your visitors directly from Telegram. All chat messages are forwarded to your Telegram chat, and your replies appear in the widget in real time.

1

Create a bot with @BotFather on Telegram and copy the bot token.

2

Open your Dashboard, go to Settings, paste the bot token and chat ID, then hit Connect.

3

Reply to any forwarded message in Telegram — your answer appears instantly in the widget chat.

Supports direct chats and group topics. You can use the same bot across multiple sites by mapping each site to a different topic in a Telegram group. Or create one Bot / Topic.
Widget not showing up?
Check your browser console. If you see a Content-Security-Policy error, your site blocks external scripts. Add floorsjs.com to your CSP:
script-src ... https://floorsjs.com
connect-src ... https://floorsjs.com wss://server.floorsjs.com
Using Cloudflare? Check your security settings — Cloudflare may block inline scripts or WebSocket connections. Whitelist the domains above.