diff --git a/client/SkribblClient.js b/client/SkribblClient.js
index 6b9e8f44d7aa3129bd9c2b7d7d9111389ee65718..31644ea5030384ef7efa8f129f0ccd17e5b9b87a 100644
--- a/client/SkribblClient.js
+++ b/client/SkribblClient.js
@@ -1,19 +1,34 @@
 import DataChannel from "./networking/DataChannel.js";
 import Signaler from "./networking/Signaler.js";
 
+/**
+ * Client/endpoint that manages a connection to a SkribblServer
+ */
 export default class SkribblClient {
 	/**
 	 * Constructs a new client for a given game ID or DataChannel.
 	 * @param {string|DataChannel} idOrDataChannel
 	 */
 	constructor(idOrDataChannel){
+		/** @type {string[]} */
+		this._players = [];
+		/** @type {((players:string[])=>void)[]} */
+		this._onPlayersListChangeCallbacks = [];
 		this._readyPromise = (async()=>{
 			this._dataChannel = idOrDataChannel instanceof DataChannel?idOrDataChannel:await Signaler.join(idOrDataChannel);
 			await this._dataChannel.waitUntilReady();
-			this._dataChannel.send("Hi there :3");
 			this._dataChannel.onMessage(message=>{
 				console.log(`message from server: "${message}"`);
-			})
+				if (message.startsWith("players add ")){
+					let name = message.split(" ").slice(2).join(" ");
+					this._players.push(name);
+					this._onPlayersListChangeCallbacks.forEach(callback=>callback(this._players));
+				}else if(message.startsWith("players list ")){
+					let json = message.split(" ").slice(2).join(" ");
+					this._players = JSON.parse(json);
+					this._onPlayersListChangeCallbacks.forEach(callback=>callback(this._players));
+				}
+			});
 		})();
 	}
 
@@ -23,4 +38,25 @@ export default class SkribblClient {
 	async waitUntilReady(){
 		return this._readyPromise;
 	}
+
+	/**
+	 * Joins the game with the given name.
+	 * @param {string} name
+	 */
+	async join(name){
+		this._dataChannel.send(`join ${name}`);
+		let message = await this._dataChannel.next();
+		if (message!=="yup"){
+			throw new Error(`Failed to join! Server response: "${message}"`);
+		}
+	}
+
+	/**
+	 * Registers a callback whenever the list of players in the game changes, and once when it is registered.
+	 * @param {(players:string[])=>void} callback
+	 */
+	onPlayersListChange(callback){
+		this._onPlayersListChangeCallbacks.push(callback);
+		callback(this._players);
+	}
 }
\ No newline at end of file
diff --git a/client/SkribblContainer.js b/client/SkribblContainer.js
index 4675aafa9ecef69a676029a0e7926aac0e3148c4..defb93a7f4b92d620b2b4c2b7192264189c8b448 100644
--- a/client/SkribblContainer.js
+++ b/client/SkribblContainer.js
@@ -1,8 +1,16 @@
+import SkribblClient from "./SkribblClient.js";
+import SkribblLobby from "./SkribblLobby.js";
+import SkribblMenu from "./SkribblMenu.js";
+
 /**
  * Custom element `<skribbl-container>` that functions as a root element for the game, containing and managing both the game and all its menus, taking care of high-level state managment.
  */
 export default class SkribblContainer extends HTMLElement {
-	constructor(){
+	/**
+	 * 
+	 * @param {SkribblClient} client
+	 */
+	constructor(client){
 		super();
 		this.attachShadow({mode:"open"});
 		this.shadowRoot.innerHTML = `
@@ -14,11 +22,26 @@ export default class SkribblContainer extends HTMLElement {
 					right: 0;
 					bottom: 0;
 					left: 0;
-					--background-color: #efefefef;
+					--background-color: #bfbfefef;
 					background-image: linear-gradient(var(--background-color),var(--background-color)), url(./res/background.png);
 				}
+				skribbl-menu, skribbl-lobby {
+					position: absolute;
+					left: 50%;
+					top: 50%;
+					transform: translate(-50%,-50%);
+				}
 			</style>
 		`;
+		(async()=>{
+			let menu = new SkribblMenu();
+			this.shadowRoot.appendChild(menu);
+			let {name} = await menu.awaitProceed();
+			await client.join(name);
+			menu.remove();
+			let lobby = new SkribblLobby(client);
+			this.shadowRoot.appendChild(lobby);
+		})();
 	}
 }
 customElements.define("skribbl-container",SkribblContainer);
\ No newline at end of file
diff --git a/client/SkribblLobby.js b/client/SkribblLobby.js
new file mode 100644
index 0000000000000000000000000000000000000000..f63d5d1dfbc6b8491707bbc76a2f44a3f1a7e611
--- /dev/null
+++ b/client/SkribblLobby.js
@@ -0,0 +1,30 @@
+import SkribblClient from "./SkribblClient.js";
+
+/**
+ * Custom element `<skribbl-lobby>` for the lobby of a game.
+ */
+export default class SkribblLobby extends HTMLElement {
+	/**
+	 * @param {SkribblClient} client
+	 */
+	constructor(client){
+		super();
+		this.attachShadow({mode:"open"});
+		this.shadowRoot.innerHTML = `
+			<style>
+				:host {
+					display: block;
+					background: #ffffff;
+					border-radius: 10px;
+					padding: 15px;
+				}
+			</style>
+			Players: <span></span>
+		`;
+		this._jens = this.shadowRoot.querySelector("span");
+		client.onPlayersListChange(players=>{
+			this._jens.innerText = players.join(", ");
+		});
+	}
+}
+customElements.define("skribbl-lobby",SkribblLobby);
\ No newline at end of file
diff --git a/client/SkribblMenu.js b/client/SkribblMenu.js
new file mode 100644
index 0000000000000000000000000000000000000000..200b10cfb3c2e65583eb83ad63935b95d07a1d05
--- /dev/null
+++ b/client/SkribblMenu.js
@@ -0,0 +1,37 @@
+/**
+ * Custom element `<skribbl-menu>` for the name selection menu that appears when first entering a game.
+ */
+export default class SkribblMenu extends HTMLElement {
+	constructor(){
+		super();
+		this.attachShadow({mode:"open"});
+		this.shadowRoot.innerHTML = `
+			<style>
+				:host {
+					display: block;
+					background: #ffffff;
+					border-radius: 10px;
+					padding: 15px;
+				}
+			</style>
+			<input type="text">
+			<button>Play</button>
+		`;
+		this._nameInput = this.shadowRoot.querySelector("input");
+		this._playButton = this.shadowRoot.querySelector("button");
+		/** @type {Promise<{name:string}>} */
+		this._proceedPromise = new Promise(resolve=>{
+			this._playButton.addEventListener("click",e=>{
+				resolve({name:this._nameInput.value});
+			});
+		});
+	}
+
+	/**
+	 * Waits until the user clicks play, then returns the name and settings he has chosen.
+	 */
+	async awaitProceed(){
+		return await this._proceedPromise;
+	}
+}
+customElements.define("skribbl-menu",SkribblMenu);
\ No newline at end of file
diff --git a/client/SkribblServer.js b/client/SkribblServer.js
index 34f20eba8d6e4339bb3412aa29901ffbe409b293..54023bcf19b8c4fa87b9e0a9909e97f293571be3 100644
--- a/client/SkribblServer.js
+++ b/client/SkribblServer.js
@@ -10,9 +10,14 @@ export default class SkribblServer {
 	 */
 	constructor(){
 		this._readyPromise = (async()=>{
+			/** @type {{name:string,dataChannel:DataChannel}[]} */
+			this._clients = [];
 			this._id = await Signaler.host(dataChannel=>{
 				this.connect(dataChannel);
 			});
+			let [endpointA,endpointB] = DataChannel.createPair();
+			this._dataChannel = endpointA;
+			this.connect(endpointB);
 		})();
 	}
 
@@ -23,6 +28,14 @@ export default class SkribblServer {
 		return this._readyPromise;
 	}
 
+	/**
+	 * A dataChannel talking to this server like any other client.
+	 * @readonly
+	 */
+	get dataChannel(){
+		return this._dataChannel;
+	}
+
 	/**
 	 * The ID others can use to connect to this server.
 	 * @readonly
@@ -44,9 +57,26 @@ export default class SkribblServer {
 	 * @param {DataChannel} dataChannel
 	 */
 	connect(dataChannel){
-		dataChannel.onMessage(message=>{
-			console.log(`message from client: "${message}"`);
-			dataChannel.send("Got your message :D");
-		});
+		(async()=>{
+			let message = await dataChannel.next();
+			console.log("message: ",message);
+			if (message.startsWith("join ")){
+				let name = message.split(" ").slice(1).join(" ");
+				if (name.length<30&&name.length>=1){
+					dataChannel.send("yup");
+					this._clients.forEach(({dataChannel})=>{
+						dataChannel.send("players add "+name);
+					});
+					this._clients.push({name,dataChannel});
+					dataChannel.send("players list "+JSON.stringify(this._clients.map(({name})=>name)))
+				}else{
+					dataChannel.send("nope");
+					dataChannel.close();
+				}
+			}else{
+				dataChannel.send("nope");
+				dataChannel.close();
+			}
+		})();
 	}
 }
\ No newline at end of file
diff --git a/client/networking/DataChannel.js b/client/networking/DataChannel.js
index 3a7e6a323fa862de8ff5161596c00dff21ace6fd..e705891cd446b43bbfaf537e39d55521a5df71a5 100644
--- a/client/networking/DataChannel.js
+++ b/client/networking/DataChannel.js
@@ -114,6 +114,30 @@ export default class DataChannel {
 			return {send,onMessage};
 		});
 	}
+
+	/**
+	 * Creates a pair of DataChannel endpoints talking directly to each other.
+	 * 
+	 * Whenever one of those sends a message, all onMessage callbacks registered on the other one get queued as a microtask.
+	 * @returns {[DataChannel,DataChannel]}
+	 */
+	static createPair(){
+		/** @type {MessageCallback[]} */
+		let onMessageCallbacksA = [];
+		/** @type {MessageCallback[]} */
+		let onMessageCallbacksB = [];
+		let endpointA = new DataChannel(()=>Promise.resolve({send:message=>{
+			onMessageCallbacksB.forEach(callback=>queueMicrotask(()=>callback(message)));
+		},onMessage:callback=>{
+			onMessageCallbacksA.push(callback);
+		}}));
+		let endpointB = new DataChannel(()=>Promise.resolve({send:message=>{
+			onMessageCallbacksA.forEach(callback=>queueMicrotask(()=>callback(message)));
+		},onMessage:callback=>{
+			onMessageCallbacksB.push(callback);
+		}}));
+		return [endpointA,endpointB];
+	}
 }
 /**
  * Class representing a connection to a server via a WebSocket.
diff --git a/client/script.js b/client/script.js
index af81585cfe7c6b6281fa7d83388e218eb233d3f2..e4f963031d1cc2b59b18265d344e57438d922377 100644
--- a/client/script.js
+++ b/client/script.js
@@ -3,10 +3,12 @@ import SkribblContainer from "./SkribblContainer.js";
 import SkribblServer from "./SkribblServer.js";
 
 document.addEventListener("DOMContentLoaded",async()=>{
+	/** @type {SkribblClient} */
+	let client;
 	if (document.location.hash){
 		document.body.innerHTML = "";
 		const gameID = document.location.hash.substring(1);
-		const client = new SkribblClient(gameID);
+		client = new SkribblClient(gameID);
 	}else{
 		/** @type {HTMLButtonElement} *///@ts-ignore
 		const button = document.getElementById("button");
@@ -17,8 +19,9 @@ document.addEventListener("DOMContentLoaded",async()=>{
 		const server = new SkribblServer();
 		await server.waitUntilReady();
 		console.log(server.url);
+		client = new SkribblClient(server.dataChannel);
 	}
-	const game = new SkribblContainer();
+	const game = new SkribblContainer(client);
 	document.body.innerHTML = "";
 	document.body.appendChild(game);
 });
\ No newline at end of file