TogetherJS is a service for your website that makes it surprisingly easy to collaborate in real-time.
Using TogetherJS two people can interact on the same page, seeing each other's cursors, edits, and browsing a site together. The TogetherJS service is included by the web site owner, and a web site can customize and configure aspects of TogetherJS's behavior on the site.
For more information and to see TogetherJS in action, visit togetherjs.com
If you want to integrate TogetherJS onto your site see the wiki and specifically Getting Started.
The remainder of this document is about contributing to TogetherJS - but reports, fixes, features, etc. Look back at those other links if you are looking for something else.
Please submit bug reports as github issues. Don't worry about labels or milestones. If you use the in-app feedback to give us a bug report that's fine too.
To see what we're planning or at least considering to do with TogetherJS, look at see our bug tracker.
TogetherJS has two main pieces:
-
The hub in
hub-worker/, which echoes messages back and forth between users. It doesn't do much: it broadcasts whatever it receives to everyone else in the room, and understands none of it. -
The client in
src/, which does all the real work.
There is no shared hub server anymore, so you'll need to host your own (see "Hosting the Hub Server" in site/docs/index.md). The recommended way is hub-worker/, a Cloudflare Workers + Durable Objects port of the relay logic that runs on Cloudflare's free plan. Note if you include TogetherJS on an https site, you must use an https/wss hub server (Cloudflare Workers handle this automatically).
The client is a set of ES modules under src/, bundled with
esbuild. To build it, install
Node 20 or newer and run:
$ npm install
$ npm run buildThat writes dist/:
togetherjs.js— the whole client, ready to drop into a page with<script src=".../togetherjs.js"></script>togetherjs.min.js— the same thing, minified, with a source maptogetherjs.esm.js— an ES module entry, forimport { TogetherJS } from "togetherjs"togetherjs.css,images/— the stylesheet and the two remaining assets (the cursor SVG and the notification sound; the icons are inline)recorder.js,walkabout.js— separate bundles, loaded on demand
To develop, run a watching build with a static server:
$ npm run devThen open examples/index.html. It expects a hub at http://localhost:8787
(run npx wrangler dev inside hub-worker/), or you can point it elsewhere
with ?hub=http://host:port.
The hub URL baked into a build comes from the HUB_URL environment variable:
$ HUB_URL=https://hub.example.com npm run buildBASE_URL does the same for the URL the client's own assets are served from;
leave it unset and the client works out where it was loaded from.
The package is not published to the npm registry, but it installs straight from GitHub:
$ npm install github:jsfiddle/togetherjs#developor, in package.json:
"dependencies": {
"togetherjs": "github:jsfiddle/togetherjs#develop"
}Any branch, tag or commit works after the #. dist/ is not checked in; npm
builds it at install time through the prepare script, so nothing else needs
to happen. (pnpm and Yarn Berry only run a git dependency's prepare if you
allow build scripts for it.)
import TogetherJS from "togetherjs";
TogetherJS.config("hubBase", "https://hub.example.com");
button.addEventListener("click", TogetherJS);Two things your own build has to take care of:
-
Assets. The client fetches
togetherjs.cssandimages/notification.oggat runtime. A drop-in<script>finds them next to its own URL; once the client is bundled into your code it cannot. Either copynode_modules/togetherjs/dist/somewhere public and assignTogetherJS.baseUrlbefore starting a session, or hand both files to your bundler and tell the client:import "togetherjs/dist/togetherjs.css"; import sound from "togetherjs/dist/images/notification.ogg?url"; // Vite TogetherJS.config({ stylesheet: false, notificationSound: sound });
stylesheetalso accepts a URL. (The/testchat command loadswalkabout.jsfrombaseUrland has no such option.) -
The hub. The hub URL baked into the build is whatever
HUB_URLwas in the environment when npm installed the package, which is usually nothing useful. SethubBaseat runtime as above.
TogetherJS.isCreator() tells you whether this client started the running
session or joined it.
The module touches window and document when it is imported, so import it
only in the browser. It also still assigns window.TogetherJS, and still
reads TogetherJSConfig_* globals if you prefer those.
The client's appearance is driven by custom properties declared on
.togetherjs. Setting one in your own stylesheet is enough — the hover,
border and muted variants are derived from the base with color-mix(), so
you do not have to restate them:
.togetherjs {
--tj-accent: #c0392b; /* buttons, active states, links */
--tj-surface: #fffdf7; /* panel and dock background */
--tj-text: #1b1a17; /* body text */
}Geometry and type have tokens too: --tj-radius, --tj-radius-lg,
--tj-space (an 8px scale), --tj-font, --tj-font-size, --tj-shadow,
--tj-button-size, --tj-avatar-size, --tj-panel-width and --tj-z, which
is the base of every z-index the client uses.
Typography is inherited from the host page by default. TogetherJSConfig_baseFont
overrides it with any CSS font-family value, including a reference to a
custom property you already define:
<script>TogetherJSConfig_baseFont = "var(--font-base)";</script>Dark mode follows prefers-color-scheme. To force one or the other, set
TogetherJSConfig_theme to "light", "dark" or "auto" (the default); the
client writes data-tj-theme on <html>, which is where cursors and click
ripples pick it up too.
Every rule in togetherjs.css is scoped to a TogetherJS root, so the client
cannot restyle the page it is embedded in. It is deliberately not in a cascade
layer: a layer would let your plain button { ... } rule win over the
client's own layout, which is not usually what anyone means. Override through
the tokens, or with a more specific selector if you need to.
Icons are inline SVG painted with currentColor, so they follow whatever text
colour applies to them.
Unit tests use Vitest and live in tests/unit/:
$ npm testEnd-to-end tests use Playwright and live in
tests/e2e/. They drive two or three real browser contexts through a session
against an in-process stand-in for the hub, covering cursors, chat, form sync
and the audio/video mesh:
$ npm run test:e2eLint with npm run lint and format with npm run format.
examples/manual/ holds pages for poking at particular behaviours by hand.
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.