Test store. Prices are the planned list prices stored as test data, license terms are drafts, and payments go to a Stripe test-mode account. No real money moves.

Dress-Up Game in HTML5: Full Code and Step-by-Step Tutorial

This dress-up game is one HTML file of 340 lines: a character drawn with canvas shapes, seven slots (skin, hair, top, bottom, shoes, hat and an extra) with several styles and colours each, randomise and reset buttons, and five themed rounds that rate your outfit with stars. Play it above, copy the code and open it in a browser.

Runs in your browser in a sandbox · 341 lines · no downloads

A dress-up game is a character, a wardrobe and a way to switch pieces on and off. It is one of the friendliest first projects for anyone who likes drawing more than physics, and it teaches two ideas every game needs: keeping game data separate from code, and drawing things in the right order. The HTML5 dress-up game code above, called Wardrobe Studio, adds a goal on top of free play: five themes, from a snow day to a birthday party, and a star rating for how well your outfit fits each one.

How to run it

  1. Press Copy code above, or Download .html.
  2. Save it as dress-up-game.html in any folder.
  3. Open the file in a browser and tap or press Enter to start.

Use ◀ and ▶ to change a piece, tap the coloured dot to change its colour, and press Done when the outfit suits the theme. You can also paste the code into the HTML game code runner, add a colour to a palette and press Run to see it straight away.

How the HTML5 dress-up game code works

The script has five parts: wardrobe data, state, layout, drawing and input. Almost everything you'd want to change as a designer lives in the data at the top, so adding a new hat doesn't mean touching the game logic.

1. The wardrobe is data

Each slot has a name, a list of items and a colour palette. Items carry tags such as warm, sun or party; the game never checks for "Beanie" by name when it rates an outfit, only for tags. That keeps the rating rules short and lets you add items without changing them.

dress-up-game.html
const SLOTS = [
  { name: "Skin", items: [{ n: "Tone" }], colors: ["#f6d7c3", "#eac0a0", "#d49a6a", "#a86b45", "#7a4a2e", "#4b2e1e"] },
  { name: "Hair", items: [{ n: "Short" }, { n: "Long" }, { n: "Curly" }, { n: "Ponytail" }, { n: "Buns" }],
    colors: ["#2b1d14", "#5a3825", "#a0522d", "#d9a05b", "#f1e3b5", "#9e9e9e", "#c0392b"] },
  { name: "Top", items: [{ n: "T-shirt", t: "sport sun" }, { n: "Tank top", t: "sun" }, { n: "Hoodie", t: "warm sport" },
    { n: "Sweater", t: "warm" }, { n: "Raincoat", t: "rain" }, { n: "Sparkle top", t: "party" }], colors: CLOTH },
  { name: "Bottom", items: [{ n: "Trousers", t: "warm rain" }, { n: "Shorts", t: "sun sport" }, { n: "Skirt", t: "party sun" },
    { n: "Joggers", t: "sport warm" }], colors: CLOTH },

The skin slot has one item and six tones, which shows a useful trick: a slot that only changes colour is just a slot with a single style. The five themes are data too, each with a tag it wants, a tag it avoids, a sky colour and a weather effect.

2. The outfit is one small object per slot

The whole state of the character is pick, an array with one { i, c } object per slot: the index of the chosen item and the index of its colour. Reset copies the default outfit back, randomise rolls new indexes, and the arrow buttons step through the list with wrap-around:

js
function cycle(slot, d) {
  const s = SLOTS[slot], p = pick[slot];
  if (s.items.length > 1) p.i = (p.i + d + s.items.length) % s.items.length;
  else p.c = (p.c + d + s.colors.length) % s.colors.length;
  bounce = 0;
}

Adding s.items.length before the % keeps the result positive when you step back from the first item, because JavaScript's % returns a negative number for negative inputs. Setting bounce to 0 starts a short squash-and-stretch on the character, so every change gets a little reaction.

Because the outfit is just indexes, it is also tiny to save or share: seven pairs of numbers describe any look.

3. Draw in units, then scale once

The character is drawn in made-up units where 100 is roughly head to feet and x = 0 is the middle. drawCharacter() works out how many pixels a unit is worth for the current screen, moves the origin to the stage and scales the canvas once. Every shape after that uses small, readable numbers like ell(0, 16, 10, 11, skin) for the head.

js
function drawCharacter(time) {
  const u = Math.min(stage.h / 126, stage.w / 80);
  const breathe = Math.sin(time * 2) * 0.4, squash = 1 + 0.05 * Math.sin(Math.min(1, bounce / 0.25) * Math.PI);
  ctx.save();
  ctx.translate(stage.x + stage.w / 2, stage.y + stage.h - 4 * u);
  ctx.scale(u * (2 - squash), u * squash);
  ctx.translate(0, -96);                      // from here on: x = 0 is the middle, y = 0 is above the head

The same scale() call also does the squash: the character gets 5% taller and slightly narrower for a quarter of a second after each change. breathe moves the head and hair up and down by less than half a unit, which is enough to make the figure feel alive.

4. Layers: draw from back to front

Canvas has no layers or z-index: whatever you draw last ends up on top. A dress-up game is therefore mostly a question of order. This one draws, from back to front: hair that hangs behind the body, a held balloon, legs, trousers or a skirt, shoes, the neck, the top, arms and sleeves, scarf, the head and face, the hair on top of the head, the hat and finally an umbrella. Here is the lower body:

js
seg(-5, 56, -5, 88, 7.5, skin); seg(5, 56, 5, 88, 7.5, skin);
const legBottom = bottom === "Shorts" ? 68 : bottom === "Skirt" ? 0 : 86;
if (bottom === "Skirt") poly(bc, [[-10.5, 52], [10.5, 52], [16, 72], [-16, 72]]);
else {
  poly(bc, [[-10.5, 52], [10.5, 52], [10, 60], [-10, 60]]);
  ctx.fillStyle = bc; ctx.fillRect(-10, 58, 9.4, legBottom - 58); ctx.fillRect(0.6, 58, 9.4, legBottom - 58);
  if (bottom === "Joggers") { ctx.fillStyle = shade(bc, -0.3); ctx.fillRect(-10, 82, 9.4, 4); ctx.fillRect(0.6, 82, 9.4, 4); seg(-9.6, 60, -9.6, 81, 1, "#fff"); seg(9.6, 60, 9.6, 81, 1, "#fff"); }
}

The legs are always drawn in skin colour first, and clothing covers as much of them as it needs: shorts stop at unit 68, trousers at 86. Boots are drawn after trousers, so they cover the trouser hems, which is exactly how real boots look. Long hair is split into two parts: the back part is drawn before the body, and the fringe and side locks after the face.

5. One palette, many shades

Every item uses the colour from its slot's palette, plus darker or lighter versions for details such as a hood lining, a beanie's folded band or a scarf's tail. shade() mixes a hex colour towards black or white, so any colour you add to a palette automatically gets matching details:

js
function shade(hex, f) {   // f < 0 darkens, f > 0 lightens
  const n = parseInt(hex.slice(1), 16), mix = (v) => Math.round(f < 0 ? v * (1 + f) : v + (255 - v) * f);
  return `rgb(${mix(n >> 16)}, ${mix((n >> 8) & 255)}, ${mix(n & 255)})`;
}
function poly(col, pts) { ctx.fillStyle = col; ctx.beginPath(); pts.forEach(([x, y]) => ctx.lineTo(x, y)); ctx.closePath(); ctx.fill(); }

6. Rating an outfit against a theme

Free play is fun for a minute; a goal makes people come back. When you press Done, rate() checks the five clothing slots. Each piece with the theme's tag adds 20 points, and each piece with the tag the theme avoids takes off 10, so sandals on a snow day cost you. The score becomes 0 to 3 stars, and the tip tells the player what to change.

js
function rate() {
  const t = theme();
  let score = 0, fits = 0, clash = null, missing = null;
  for (const slot of RATED) {
    const tags = item(slot).t || "";
    if (tags.includes(t.want)) { score += 20; fits++; } else if (!missing) missing = TIP[slot];
    if (tags.includes(t.avoid)) { score -= 10; clash = clash || item(slot).n; }
  }
  score = Math.max(0, score);
  const got = score >= 80 ? 3 : score >= 50 ? 2 : score >= 20 ? 1 : 0;
  stars += got;

Colours, hair and skin never affect the score. That is deliberate: the rating is about suiting the weather or the event, and the player's own style stays their own. The five themes come in a random order each game, shuffled with a Fisher–Yates loop, and the final screen shows your total out of 15 stars.

7. Buttons drawn on the canvas

The controls are drawn on the same canvas as the character. resize() builds a list of button rectangles, each with an act function, and the pointer handler finds the one under the finger:

js
canvas.addEventListener("pointerdown", (e) => {
  if (state !== "dress") { next(); return; }
  const b = buttons.find((b) => e.clientX >= b.x && e.clientX <= b.x + b.w && e.clientY >= b.y && e.clientY <= b.y + b.h);
  if (b) b.act();
});

The keyboard works the same list in a different way: up and down pick a row, left and right change the piece, Space changes its colour, R randomises, Backspace resets and Enter rates the outfit.

How to code a dress-up game with HTML5 and CSS instead

Many dress-up games aren't drawn on a canvas at all. The classic HTML and CSS approach stacks transparent PNG images of the same size inside one container, each positioned with position: absolute and ordered with z-index: body at the bottom, then trousers, top, hair and hat. Changing a piece means swapping one image's src; changing a colour means switching to another image or tinting it with a CSS filter such as hue-rotate(). Inline SVG is a third option, where every piece is a vector group whose fill you change from JavaScript.

Canvas (this file)HTML and CSS layersInline SVG
ArtShapes in code, or images drawn with drawImagePNG images made in any art toolVector pieces exported from a drawing tool
Layer orderThe order of your drawing callsz-index on each imageThe order of the elements
RecolouringAny colour, from codeSeparate images or CSS filtersChange the fill attribute
Clicking a pieceYour own hit testsBuilt in, but transparent areas still catch clicksBuilt in: each shape is an element
Saving a pictureBuilt in with toBlobNeeds extra workNeeds extra work

For hand-drawn art the CSS route is often quicker to build, and each piece is a real element you can label for screen readers. A canvas is better when you want animation, particle effects or one-tap "save my outfit" images. Our guide to canvas vs SVG vs DOM for HTML5 games goes deeper into the trade-offs. Whichever you choose, the data and rating code from steps 1, 2 and 6 stay the same.

Make it work on phones

The layout changes with the shape of the screen. When the window is taller than it is wide, the character takes the top of the screen and the seven control rows sit underneath, within reach of a thumb; on a wide screen the controls move to the right. Both layouts come from the same resize() function, so rotating a phone rebuilds the buttons in their new places. Every control is a full-size button rather than a small icon, and touch-action: none stops the page from zooming when someone taps quickly. For more on screen sizes and touch, see our guide to HTML5 mobile game development.

Ideas to extend it

  • Let players drag items from a wardrobe shelf onto the character instead of using arrows.
  • Add a "Save picture" button that calls canvas.toBlob() and downloads a PNG of the outfit.
  • Replace the canvas shapes with your own drawings: load PNG images and draw them with drawImage in the same back-to-front order.
  • Add a background slot (park, beach, stage) that changes the scene.
  • Add a timer mode: dress for each theme in 20 seconds.
  • Play a soft "pop" with the Web Audio API on every change, started after the first tap.

From prototype to a game you can sell

This file is complete and a good base for learning, a class project or a gift for a young player. A dress-up game that sells, or that a game portal will accept, needs much more:

This dress-up fileA production HTML5 game
Content7 slots, 5 themesHundreds of items, unlockable sets, events
Art and soundCanvas shapes, no audioOriginal illustrated art, animation, music and effects
SavesOutfit lost on reloadWardrobe, unlocks and settings saved per player
PlatformsDesktop and phone browsersPortal SDK hooks, ads, several languages
AccessibilityKeyboard and touchScreen reader labels, colour-blind friendly palettes
QualityTested by hand and in a sandboxed playerUnit tests and cross-browser end-to-end tests
LicenceMIT, do anythingA commercial licence for your use

We don't sell a dress-up game. The closest games in our catalog are about decorating and customising: House Renovator, where you clean, repair, paint and decorate rooms, and Merge Café Story, where you restore a café. Both include cosmetics and collections, the same "pick a look" idea as dress-up slots, and both ship as TypeScript projects on Phaser 4.2.1:

If you want a dress-up game in the catalog, suggest it on our roadmap, or request an exclusive game built only for you. You can also browse all our HTML5 games or try another small project from the free HTML game code list, such as the memory match game.

The complete code

One file, no libraries. Save it as dress-up-game.html and open it in any browser. Free to use under the MIT license: copy it, change it, ship it.

dress-up-game.html
<!doctype html>
<!-- SPDX-License-Identifier: MIT -->
<!-- Wardrobe Studio: a dress-up game in one HTML file. Free to use, change and ship (MIT license). -->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Wardrobe Studio (HTML5 dress-up game)</title>
<style>
  html, body { margin: 0; height: 100%; background: #fdf6ec; overflow: hidden; touch-action: none; user-select: none; -webkit-user-select: none; }
  canvas { display: block; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<script>
// ----- Wardrobe data: every slot has styles (items) and colours. Tags are used to rate an outfit. -----
const CLOTH = ["#e63946", "#f4a261", "#ffd166", "#2a9d8f", "#457b9d", "#f1faee", "#264653", "#c77dff"];
const SLOTS = [
  { name: "Skin", items: [{ n: "Tone" }], colors: ["#f6d7c3", "#eac0a0", "#d49a6a", "#a86b45", "#7a4a2e", "#4b2e1e"] },
  { name: "Hair", items: [{ n: "Short" }, { n: "Long" }, { n: "Curly" }, { n: "Ponytail" }, { n: "Buns" }],
    colors: ["#2b1d14", "#5a3825", "#a0522d", "#d9a05b", "#f1e3b5", "#9e9e9e", "#c0392b"] },
  { name: "Top", items: [{ n: "T-shirt", t: "sport sun" }, { n: "Tank top", t: "sun" }, { n: "Hoodie", t: "warm sport" },
    { n: "Sweater", t: "warm" }, { n: "Raincoat", t: "rain" }, { n: "Sparkle top", t: "party" }], colors: CLOTH },
  { name: "Bottom", items: [{ n: "Trousers", t: "warm rain" }, { n: "Shorts", t: "sun sport" }, { n: "Skirt", t: "party sun" },
    { n: "Joggers", t: "sport warm" }], colors: CLOTH },
  { name: "Shoes", items: [{ n: "Sneakers", t: "sport party" }, { n: "Boots", t: "warm" }, { n: "Rain boots", t: "rain" },
    { n: "Sandals", t: "sun" }], colors: CLOTH },
  { name: "Hat", items: [{ n: "No hat" }, { n: "Cap", t: "sport sun" }, { n: "Beanie", t: "warm" }, { n: "Sun hat", t: "sun" },
    { n: "Party hat", t: "party" }, { n: "Rain hat", t: "rain" }], colors: CLOTH },
  { name: "Extra", items: [{ n: "Nothing" }, { n: "Scarf", t: "warm" }, { n: "Sunglasses", t: "sun" }, { n: "Umbrella", t: "rain" },
    { n: "Balloon", t: "party" }, { n: "Ball", t: "sport" }], colors: CLOTH },
];
const THEMES = [
  { name: "Snow day", want: "warm", avoid: "sun", sky: "#dceaf7", fx: "snow" },
  { name: "Beach trip", want: "sun", avoid: "warm", sky: "#ffeab8", fx: "sun" },
  { name: "Rainy walk", want: "rain", avoid: "sun", sky: "#c9d3dc", fx: "rain" },
  { name: "Birthday party", want: "party", avoid: "rain", sky: "#fadbe8", fx: "party" },
  { name: "Sports day", want: "sport", avoid: "party", sky: "#d6f0d2", fx: "none" },
];
const RATED = [2, 3, 4, 5, 6];               // slots that count for the theme: top, bottom, shoes, hat, extra
const TIP = { 2: "top", 3: "bottom", 4: "pair of shoes", 5: "hat", 6: "accessory" };
const DEFAULT = [[0, 1], [0, 1], [0, 4], [0, 6], [0, 5], [0, 0], [0, 0]];   // [item, colour] per slot

const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
let W = 0, H = 0, hud = 52, stage, panel, buttons = [];

// ----- State -----
let state = "ready";                         // ready → dress → rated → … (5 themes) → over
let pick = DEFAULT.map(([i, c]) => ({ i, c })), sel = 2, round = 0, order = [], stars = 0, rating = null;
let bounce = 1, last = 0;
const flakes = Array.from({ length: 40 }, () => ({ x: Math.random(), y: Math.random(), s: 0.5 + Math.random() }));
const theme = () => THEMES[order[round]];
const item = (slot) => SLOTS[slot].items[pick[slot].i];
const color = (slot) => SLOTS[slot].colors[pick[slot].c];

function newGame() {
  order = [0, 1, 2, 3, 4];                     // play the themes in a random order (Fisher–Yates shuffle)
  for (let i = order.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [order[i], order[j]] = [order[j], order[i]]; }
  round = 0; stars = 0; reset(); state = "dress";
}
function reset() { pick = DEFAULT.map(([i, c]) => ({ i, c })); bounce = 0; }
function randomise() {
  pick = SLOTS.map((s) => ({ i: Math.floor(Math.random() * s.items.length), c: Math.floor(Math.random() * s.colors.length) }));
  bounce = 0;
}
// Arrows change the style; the skin row has one style, so its arrows change the colour.
function cycle(slot, d) {
  const s = SLOTS[slot], p = pick[slot];
  if (s.items.length > 1) p.i = (p.i + d + s.items.length) % s.items.length;
  else p.c = (p.c + d + s.colors.length) % s.colors.length;
  bounce = 0;
}
function recolor(slot) { pick[slot].c = (pick[slot].c + 1) % SLOTS[slot].colors.length; bounce = 0; }

// ----- Rating: +20 for each piece that suits the theme, −10 for each that clashes -----
function rate() {
  const t = theme();
  let score = 0, fits = 0, clash = null, missing = null;
  for (const slot of RATED) {
    const tags = item(slot).t || "";
    if (tags.includes(t.want)) { score += 20; fits++; } else if (!missing) missing = TIP[slot];
    if (tags.includes(t.avoid)) { score -= 10; clash = clash || item(slot).n; }
  }
  score = Math.max(0, score);
  const got = score >= 80 ? 3 : score >= 50 ? 2 : score >= 20 ? 1 : 0;
  stars += got;
  const tip = clash ? `${clash} on a ${t.name.toLowerCase()}? Not a great fit.` : missing ? `Tip: try a different ${missing}.` : "Perfect match!";
  rating = { score, got, fits, tip };
  state = "rated";
}
function next() {
  if (state === "ready") newGame();
  else if (state === "dress") rate();
  else if (state === "rated") { round++; if (round >= THEMES.length) state = "over"; else state = "dress"; }
  else if (state === "over") newGame();
}

// ----- Layout: character on top and controls below on phones, side by side on wide screens -----
function resize() {
  const dpr = window.devicePixelRatio || 1;
  W = window.innerWidth; H = window.innerHeight;
  canvas.style.width = W + "px"; canvas.style.height = H + "px";
  canvas.width = Math.round(W * dpr); canvas.height = Math.round(H * dpr);
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  if (W < H * 0.95) {
    const ph = Math.min(H * 0.54, 8.4 * 46);
    panel = { x: 10, y: H - ph - 8, w: W - 20, h: ph };
    stage = { x: 0, y: hud, w: W, h: panel.y - hud };
  } else {
    const pw = Math.min(440, W * 0.5);
    panel = { x: W - pw - 16, y: hud + 12, w: pw, h: H - hud - 24 };
    stage = { x: 0, y: hud, w: panel.x, h: H - hud };
  }
  const rh = Math.min(52, panel.h / 8.3), b = rh * 0.84, top = panel.y + (panel.h - rh * 8.3) / 2;
  buttons = [];
  SLOTS.forEach((s, i) => {
    const y = top + i * rh;
    buttons.push({ x: panel.x, y, w: b, h: b, row: i, label: "◀", act: () => { sel = i; cycle(i, -1); } });
    buttons.push({ x: panel.x + panel.w - b * 2 - 8, y, w: b, h: b, row: i, label: "▶", act: () => { sel = i; cycle(i, 1); } });
    buttons.push({ x: panel.x + panel.w - b, y, w: b, h: b, row: i, swatch: true, act: () => { sel = i; recolor(i); } });
  });
  const by = top + 7 * rh + rh * 0.3, bw = (panel.w - 16) / 3;
  [["Random", randomise], ["Reset", reset], ["Done ✓", next]].forEach(([label, act], k) =>
    buttons.push({ x: panel.x + k * (bw + 8), y: by, w: bw, h: rh * 0.9, label, act, big: true }));
  draw(0);
}

// ----- Drawing helpers (the character is drawn in units: 100 = head to feet) -----
function shade(hex, f) {   // f < 0 darkens, f > 0 lightens
  const n = parseInt(hex.slice(1), 16), mix = (v) => Math.round(f < 0 ? v * (1 + f) : v + (255 - v) * f);
  return `rgb(${mix(n >> 16)}, ${mix((n >> 8) & 255)}, ${mix(n & 255)})`;
}
function poly(col, pts) { ctx.fillStyle = col; ctx.beginPath(); pts.forEach(([x, y]) => ctx.lineTo(x, y)); ctx.closePath(); ctx.fill(); }
function ell(x, y, rx, ry, col) { ctx.fillStyle = col; ctx.beginPath(); ctx.ellipse(x, y, rx, ry, 0, 0, Math.PI * 2); ctx.fill(); }
function seg(x1, y1, x2, y2, w, col) { ctx.strokeStyle = col; ctx.lineWidth = w; ctx.lineCap = "round"; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); }
function rrect(x, y, w, h, r) {
  ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r);
  ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath();
}

function drawCharacter(time) {
  const u = Math.min(stage.h / 126, stage.w / 80);
  const breathe = Math.sin(time * 2) * 0.4, squash = 1 + 0.05 * Math.sin(Math.min(1, bounce / 0.25) * Math.PI);
  ctx.save();
  ctx.translate(stage.x + stage.w / 2, stage.y + stage.h - 4 * u);
  ctx.scale(u * (2 - squash), u * squash);
  ctx.translate(0, -96);                      // from here on: x = 0 is the middle, y = 0 is above the head
  const skin = color(0), hair = color(1), top = item(2).n, bottom = item(3).n, shoes = item(4).n, hat = item(5).n, extra = item(6).n;
  const tc = color(2), bc = color(3), sc = color(4), hc = color(5), xc = color(6), hairStyle = item(1).n;
  ell(0, 95, 20, 2.5, "rgba(0, 0, 0, 0.12)");  // shadow
  // Hair that hangs behind the head and body.
  if (hairStyle === "Long") { rrect(-12, 10 + breathe, 24, 30, 9); ctx.fillStyle = hair; ctx.fill(); }
  if (hairStyle === "Curly") for (let a = 0; a < 12; a++) ell(Math.cos(a / 1.9) * 11, 16 + Math.sin(a / 1.9) * 10 + breathe, 5, 5, hair);
  if (hairStyle === "Ponytail") { seg(10, 12 + breathe, 15, 30 + breathe, 6, hair); ell(9, 11 + breathe, 3, 3, shade(hair, -0.3)); }
  if (hairStyle === "Buns") { ell(-9, 7 + breathe, 5, 5, hair); ell(9, 7 + breathe, 5, 5, hair); }
  if (extra === "Balloon") { seg(-17, 56, -27, 14, 0.6, "#555"); ell(-27, 6, 8, 10, xc); ell(-30, 3, 2, 3, "rgba(255,255,255,0.5)"); }
  // Legs, then what covers them.
  seg(-5, 56, -5, 88, 7.5, skin); seg(5, 56, 5, 88, 7.5, skin);
  const legBottom = bottom === "Shorts" ? 68 : bottom === "Skirt" ? 0 : 86;
  if (bottom === "Skirt") poly(bc, [[-10.5, 52], [10.5, 52], [16, 72], [-16, 72]]);
  else {
    poly(bc, [[-10.5, 52], [10.5, 52], [10, 60], [-10, 60]]);
    ctx.fillStyle = bc; ctx.fillRect(-10, 58, 9.4, legBottom - 58); ctx.fillRect(0.6, 58, 9.4, legBottom - 58);
    if (bottom === "Joggers") { ctx.fillStyle = shade(bc, -0.3); ctx.fillRect(-10, 82, 9.4, 4); ctx.fillRect(0.6, 82, 9.4, 4); seg(-9.6, 60, -9.6, 81, 1, "#fff"); seg(9.6, 60, 9.6, 81, 1, "#fff"); }
  }
  for (const s of [-1, 1]) {                  // shoes
    const x = s * 5;
    if (shoes === "Sandals") { const fx = x + s * 1.5; ell(fx, 91, 5.5, 2.5, skin); seg(fx - 3.5, 89.5, fx + 3.5, 89.5, 1.6, sc); seg(fx, 89.5, fx + s * 2, 92, 1.4, sc); continue; }
    const h = shoes === "Rain boots" ? 18 : shoes === "Boots" ? 12 : 0;
    if (h) { ctx.fillStyle = sc; ctx.fillRect(x - 4.5, 88 - h, 9, h); }
    rrect(x - 5.5 + s, 86, 11, 6.5, 2.5); ctx.fillStyle = sc; ctx.fill();   // toes point slightly outwards
    if (shoes === "Sneakers") { ctx.fillStyle = "#fff"; ctx.fillRect(x - 5 + s, 90.5, 10, 1.6); }
    if (shoes === "Rain boots") seg(x - 2.5, 74, x - 2.5, 84, 1.2, "rgba(255,255,255,0.6)");
    if (h) { ctx.fillStyle = shade(sc, -0.35); ctx.fillRect(x - 5 + s, 91.5, 10, 1.5); }
  }
  // Torso and arms.
  seg(-3, 24, 3, 24, 6, skin);                 // neck
  const long = top === "Hoodie" || top === "Sweater" || top === "Raincoat", bottomY = top === "Raincoat" ? 66 : 57;
  const shoulder = top === "Tank top" ? 9 : 12.5;
  if (top === "Hoodie") ell(0, 28, 9, 4, shade(tc, -0.25));
  poly(tc, [[-shoulder, 28], [shoulder, 28], [12.5, 31], [11, bottomY], [-11, bottomY], [-12.5, 31]]);
  if (top === "Tank top") { poly(skin, [[-3.5, 28], [3.5, 28], [0, 33]]); }
  for (const s of [-1, 1]) {
    seg(s * 13, 31, s * 17, 55, 5.5, skin);
    if (top !== "Tank top") seg(s * 13, 31, s * (long ? 16.6 : 14.2), long ? 52 : 39, 7, shade(tc, -0.08));
    if (top === "Sweater") seg(s * 16.2, 50, s * 16.7, 53, 7, shade(tc, -0.3));
    ell(s * 17.2, 57, 3, 3, skin);
  }
  if (top === "Hoodie") { ctx.fillStyle = shade(tc, -0.2); ctx.fillRect(-6, 44, 12, 7); seg(-2, 29, -2.5, 38, 0.8, "#fff"); seg(2, 29, 2.5, 38, 0.8, "#fff"); }
  if (top === "Sweater") { ctx.strokeStyle = shade(tc, 0.5); ctx.lineWidth = 1.5; ctx.beginPath(); for (let x = -11; x <= 11; x += 2.75) ctx.lineTo(x, 38 + ((x + 11) / 2.75 % 2 ? 2 : -1)); ctx.stroke(); ctx.fillStyle = shade(tc, -0.3); ctx.fillRect(-11, bottomY - 3, 22, 3); }
  if (top === "Raincoat") { poly(shade(tc, -0.2), [[-5, 27], [0, 33], [-3, 34]]); poly(shade(tc, -0.2), [[5, 27], [0, 33], [3, 34]]); for (const y of [40, 48, 56]) ell(0, y, 1.1, 1.1, shade(tc, -0.45)); }
  if (top === "Sparkle top") for (let k = 0; k < 6; k++) ell(-7 + (k % 3) * 7, 36 + Math.floor(k / 3) * 10, 1.2 + 0.5 * Math.sin(time * 5 + k), 1.2 + 0.5 * Math.sin(time * 5 + k), "#fff");
  if (extra === "Scarf") { rrect(-8, 25, 16, 5, 2); ctx.fillStyle = xc; ctx.fill(); poly(shade(xc, -0.15), [[3, 28], [7.5, 29], [8, 43], [4, 43]]); }
  if (extra === "Ball") { ell(19, 61, 5.5, 5.5, xc); seg(13.5, 61, 24.5, 61, 0.8, "#333"); seg(19, 55.5, 19, 66.5, 0.8, "#333"); }
  // Head and face.
  ctx.translate(0, breathe);
  ell(-10, 18, 2.2, 3, skin); ell(10, 18, 2.2, 3, skin); ell(0, 16, 10, 11, skin);
  if (extra === "Sunglasses") { rrect(-8.5, 13.5, 7, 4.5, 1.5); ctx.fillStyle = "#222"; ctx.fill(); rrect(1.5, 13.5, 7, 4.5, 1.5); ctx.fill(); seg(-1.5, 15, 1.5, 15, 0.8, "#222"); }
  else { ell(-3.8, 16, 1.2, 1.5, "#2b2b2b"); ell(3.8, 16, 1.2, 1.5, "#2b2b2b"); }
  ell(-6, 20.5, 2, 1.2, "rgba(255, 120, 120, 0.35)"); ell(6, 20.5, 2, 1.2, "rgba(255, 120, 120, 0.35)");
  ctx.strokeStyle = "#7a3b2e"; ctx.lineWidth = 0.9; ctx.beginPath(); ctx.arc(0, 20, 3, 0.15 * Math.PI, 0.85 * Math.PI); ctx.stroke();
  // Hair on top of the head.
  ctx.fillStyle = hair; ctx.beginPath(); ctx.ellipse(0, 12, 10.6, 7.5, 0, Math.PI, 0); ctx.fill();
  if (hairStyle === "Short") poly(hair, [[-10.5, 12], [-4, 12], [-7, 16]]);
  if (hairStyle === "Long") { ctx.fillRect(-11, 11, 3.5, 18); ctx.fillRect(7.5, 11, 3.5, 18); }
  if (hairStyle === "Ponytail") ell(0, 6, 6, 2.5, hair);
  // Hats sit on top of the hair.
  if (hat === "Cap") { ctx.fillStyle = hc; ctx.beginPath(); ctx.ellipse(0, 10, 10.5, 7, 0, Math.PI, 0); ctx.fill(); ell(8, 10, 9, 1.8, shade(hc, -0.2)); }
  if (hat === "Beanie") { ctx.fillStyle = hc; ctx.beginPath(); ctx.ellipse(0, 11, 11, 9, 0, Math.PI, 0); ctx.fill(); rrect(-11.5, 8.5, 23, 4.5, 2); ctx.fillStyle = shade(hc, -0.25); ctx.fill(); ell(0, 1.5, 3, 3, shade(hc, 0.5)); }
  if (hat === "Sun hat") { ell(0, 8.5, 20, 3.5, hc); ctx.fillStyle = hc; ctx.beginPath(); ctx.ellipse(0, 8, 9, 7, 0, Math.PI, 0); ctx.fill(); ctx.fillStyle = shade(hc, -0.35); ctx.fillRect(-9, 5.5, 18, 2.2); }
  if (hat === "Party hat") { poly(hc, [[-7, 7], [7, 7], [0, -12]]); ell(-2, 1, 1.2, 1.2, "#fff"); ell(2, -4, 1.2, 1.2, "#fff"); ell(1.5, 4, 1.2, 1.2, "#fff"); ell(0, -12.5, 2.5, 2.5, shade(hc, 0.5)); }
  if (hat === "Rain hat") { poly(hc, [[-8, 0], [8, 0], [10, 8], [-10, 8]]); poly(shade(hc, -0.2), [[-10, 7.5], [10, 7.5], [15, 11], [-15, 11]]); }
  ctx.translate(0, -breathe);
  if (extra === "Umbrella") {
    seg(17.2, 57, 17.2, -2, 1.2, "#555");
    ctx.fillStyle = xc; ctx.beginPath(); ctx.arc(17.2, -2, 22, Math.PI, 0); ctx.fill();
    for (let k = -1; k <= 1; k++) ell(17.2 + k * 14.7, -2, 7.3, 2.2, shade(xc, -0.2));
  }
  ctx.restore();
}

function drawWeather(time) {
  const t = theme() || THEMES[3];          // the start screen borrows the party backdrop
  ctx.fillStyle = t.sky; ctx.fillRect(stage.x, stage.y, stage.w, stage.h);
  ctx.fillStyle = "rgba(0, 0, 0, 0.06)"; ctx.fillRect(stage.x, stage.y + stage.h * 0.82, stage.w, stage.h * 0.18);
  if (t.fx === "sun") ell(stage.x + stage.w * 0.15, stage.y + stage.h * 0.15, 22, 22, "#ffcf33");
  for (const f of t.fx === "snow" || t.fx === "rain" || t.fx === "party" ? flakes : []) {
    const speed = t.fx === "rain" ? 0.9 : 0.08;
    const y = stage.y + ((f.y + time * speed * f.s) % 1) * stage.h, x = stage.x + f.x * stage.w;
    if (t.fx === "snow") ell(x, y, 2.5 * f.s, 2.5 * f.s, "#fff");
    else if (t.fx === "rain") seg(x, y, x - 2, y + 10, 1.5, "rgba(80, 110, 160, 0.5)");
    else { ctx.fillStyle = CLOTH[Math.floor(f.x * 8)]; ctx.fillRect(x, y, 5, 5); }
  }
}

function text(str, x, y, size, col, align = "center", weight = 700) {
  ctx.font = `${weight} ${size}px system-ui, sans-serif`;
  ctx.fillStyle = col; ctx.textAlign = align; ctx.textBaseline = "middle"; ctx.fillText(str, x, y);
}
function star(x, y, r, filled) {
  ctx.beginPath();
  for (let i = 0; i < 10; i++) { const a = -Math.PI / 2 + (i * Math.PI) / 5, d = i % 2 ? r * 0.45 : r; ctx.lineTo(x + Math.cos(a) * d, y + Math.sin(a) * d); }
  ctx.closePath(); ctx.fillStyle = filled ? "#ffb703" : "#e5e0d8"; ctx.fill();
}

function draw(time) {
  ctx.fillStyle = "#fdf6ec"; ctx.fillRect(0, 0, W, H);
  drawWeather(time);
  drawCharacter(time);
  const fs = Math.max(14.5, Math.min(18, W / 26));
  // Controls: one row per slot, then Random / Reset / Done.
  for (const b of buttons) {
    if (b.swatch) {
      if (b.row === sel && state === "dress") { rrect(panel.x - 4, b.y - 3, panel.w + 8, b.h + 6, 10); ctx.strokeStyle = "#e76f51"; ctx.lineWidth = 2; ctx.stroke(); }
      ell(b.x + b.w / 2, b.y + b.h / 2, b.h * 0.36, b.h * 0.36, SLOTS[b.row].colors[pick[b.row].c]);
      ctx.strokeStyle = "#5c4d3c"; ctx.lineWidth = 1.5; ctx.stroke();
      continue;
    }
    rrect(b.x, b.y, b.w, b.h, 8); ctx.fillStyle = b.label === "Done ✓" ? "#e76f51" : b.big ? "#3d405b" : "#ece4d8"; ctx.fill();
    text(b.label, b.x + b.w / 2, b.y + b.h / 2, b.big ? fs : fs * 0.9, b.big ? "#fff" : "#3d405b");
    if (b.label === "◀") {
      const cx = (b.x + b.w + buttons[buttons.indexOf(b) + 1].x) / 2;
      text(SLOTS[b.row].name, cx, b.y + b.h * 0.3, fs * 0.72, "#8a7f72", "center", 600);
      text(SLOTS[b.row].items.length > 1 ? item(b.row).n : `Tone ${pick[b.row].c + 1}`, cx, b.y + b.h * 0.68, fs, "#3d405b");
    }
  }
  // Top bar: the theme and the stars so far.
  ctx.fillStyle = "#3d405b"; ctx.fillRect(0, 0, W, hud);
  if (state !== "ready") {
    const t = theme() || THEMES[0];
    text(state === "over" ? "Show's over" : `Dress for: ${t.name}`, 12, hud / 2 - 7, fs * 1.05, "#fff", "left");
    text(`Theme ${Math.min(round + 1, 5)} of 5`, 12, hud / 2 + 11, fs * 0.72, "#c9c6d8", "left", 500);
  } else text("Wardrobe Studio", 12, hud / 2, fs * 1.1, "#fff", "left");
  star(W - 70, hud / 2, 11, true);
  text(`${stars}/15`, W - 12, hud / 2, fs, "#fff", "right");

  if (state === "dress") return;
  ctx.fillStyle = "rgba(40, 30, 50, 0.6)"; ctx.fillRect(0, hud, W, H - hud);
  const cw = Math.min(W - 32, 380), ch = 230, cx = W / 2, cy = hud + (H - hud) / 2;
  rrect(cx - cw / 2, cy - ch / 2, cw, ch, 16); ctx.fillStyle = "#fffaf3"; ctx.fill();
  const line = (s, dy, size, col, w) => text(s, cx, cy + dy, size, col, "center", w);
  if (state === "ready") {
    line("Wardrobe Studio", -80, fs * 1.6, "#e76f51", 800);
    line("Dress the model for 5 themes.", -40, fs, "#3d405b", 500);
    line("◀ ▶ change a piece, the dot its colour.", -14, fs * 0.85, "#3d405b", 500);
    line("Keys: ↑↓ row · ←→ piece · Space colour", 12, fs * 0.8, "#8a7f72", 500);
    line("R random · Enter done", 34, fs * 0.8, "#8a7f72", 500);
    line("Tap or press Enter to start", 80, fs, "#e76f51");
  } else if (state === "rated") {
    line(theme().name, -82, fs * 1.3, "#3d405b", 800);
    for (let k = 0; k < 3; k++) star(cx + (k - 1) * 46, cy - 36, 20, k < rating.got);
    line(`${rating.score} / 100 · ${rating.fits} of 5 pieces fit`, 10, fs, "#3d405b", 600);
    line(rating.tip, 38, fs * 0.85, "#8a7f72", 500);
    line(round + 1 < THEMES.length ? "Tap or press Enter for the next theme" : "Tap or press Enter to see your total", 82, fs * 0.9, "#e76f51");
  } else {
    line("Show's over!", -78, fs * 1.6, "#e76f51", 800);
    line(`You earned ${stars} of 15 stars`, -30, fs * 1.1, "#3d405b", 700);
    line(stars >= 13 ? "Style star!" : stars >= 9 ? "Great eye for an outfit." : "Keep experimenting!", 4, fs, "#8a7f72", 500);
    line("Tap or press Enter to play again", 76, fs, "#e76f51");
  }
}

// ----- Loop -----
function loop(now) {
  const dt = Math.min(0.1, (now - last) / 1000);
  last = now;
  bounce += dt;
  draw(now / 1000);
  requestAnimationFrame(loop);
}

// ----- Input: tap the buttons, or use the keyboard -----
canvas.addEventListener("pointerdown", (e) => {
  if (state !== "dress") { next(); return; }
  const b = buttons.find((b) => e.clientX >= b.x && e.clientX <= b.x + b.w && e.clientY >= b.y && e.clientY <= b.y + b.h);
  if (b) b.act();
});
window.addEventListener("keydown", (e) => {
  const k = e.code;
  if (k === "Enter" || (k === "Space" && state !== "dress")) { e.preventDefault(); next(); return; }
  if (state !== "dress") return;
  if (k === "ArrowUp" || k === "KeyW") sel = (sel + SLOTS.length - 1) % SLOTS.length;
  else if (k === "ArrowDown" || k === "KeyS") sel = (sel + 1) % SLOTS.length;
  else if (k === "ArrowLeft" || k === "KeyA") cycle(sel, -1);
  else if (k === "ArrowRight" || k === "KeyD") cycle(sel, 1);
  else if (k === "Space" || k === "KeyC") recolor(sel);
  else if (k === "KeyR") randomise();
  else if (k === "Backspace" || k === "Delete") reset();
  else return;
  e.preventDefault();
});

window.addEventListener("resize", resize);
resize();
requestAnimationFrame((t) => { last = t; loop(t); });
</script>
</body>
</html>

Frequently asked questions

Can I use this dress-up game code in my own project?

Yes. The file is MIT-licensed: copy it, change it and use it in personal or commercial projects, and keep the licence line at the top. Our paid games use separate commercial licences, explained on the licensing page.

How do I use my own clothing images?

Draw each piece on a transparent PNG at the same size as the character, load them with new Image() and draw them with ctx.drawImage() inside drawCharacter(), in the same back-to-front order. MDN's drawImage reference explains the options. Wait for every image's load event before the first draw.

How do I let players save their outfit as a picture?

Call canvas.toBlob() and download the result through a temporary link, as shown in MDN's toBlob reference. Hide the buttons first, or draw the character onto a second, off-screen canvas so the picture shows only the outfit.

How do I put the dress-up game on my website?

Upload dress-up-game.html to your web host and link to it, or show it inside a page with an <iframe>. Our guide to embedding an HTML5 game in a website covers WordPress, Wix and Google Sites.

Is it suitable for children?

The content is neutral and every outfit is everyday clothing. The file makes no network requests, stores nothing and shows no ads, so it collects no data from players. If you add ads, analytics or accounts, check the rules for children's apps where your players live.

We make and sell HTML5 game source code; links to our games are marked as ours.