Skip to content
JavaScript js canvas 5 min read

Colors, Fills & Line Styles

Every shape you draw on a <canvas> is painted with the context’s current style state. Before you call fill() or stroke(), you set properties like fillStyle, strokeStyle, and lineWidth, and the canvas applies them to whatever you draw next. Mastering these properties is what separates a flat wireframe sketch from a polished, intentional graphic — and because the canvas is stateful, understanding when a style takes effect is just as important as knowing the property names.

Setting fill and stroke colors

The two most fundamental style properties are fillStyle (the color used by fill() and fillRect()) and strokeStyle (the color used by stroke() and strokeRect()). Both accept any valid CSS color string: named colors, hex, rgb(), rgba(), hsl(), and hsla().

const canvas = document.querySelector("#scene");
const ctx = canvas.getContext("2d");

ctx.fillStyle = "tomato";              // named color
ctx.fillStyle = "#3b82f6";             // hex
ctx.fillStyle = "rgb(59 130 246)";     // modern space-separated rgb
ctx.fillStyle = "hsl(217 91% 60%)";    // hsl

ctx.strokeStyle = "rgba(0, 0, 0, 0.5)"; // 50% opaque black

Styles are sticky: once you set fillStyle, every subsequent fill uses that color until you change it. There is no per-shape color argument — you must reassign the property before drawing a differently-colored shape.

Tip: fillStyle and strokeStyle also accept CanvasGradient and CanvasPattern objects, not just strings. See Gradients & Patterns for those.

Transparency: globalAlpha vs rgba

There are two ways to draw semi-transparent content, and they behave differently. An rgba() (or hsla()) color sets the opacity of that single style. globalAlpha, by contrast, is a multiplier applied to everything drawn — fills, strokes, images, and even gradients — until you change it back.

ApproachScopeRangeTypical use
rgba() / hsla() alphaOne fill or stroke color01A single translucent shape
globalAlphaAll drawing operations01Fading a whole layer or group
ctx.globalAlpha = 0.4;        // affects everything below
ctx.fillStyle = "blue";
ctx.fillRect(10, 10, 80, 80); // drawn at 40% opacity
ctx.fillStyle = "rgba(255, 0, 0, 0.5)";
ctx.fillRect(60, 60, 80, 80); // 0.4 * 0.5 = 0.2 effective alpha
ctx.globalAlpha = 1;          // reset so later draws are opaque

Note how the two combine: the red rectangle’s effective opacity is globalAlpha * rgba alpha. Forgetting to reset globalAlpha back to 1 is a classic source of “why is everything faded?” bugs.

Line width, caps, and joins

When you stroke a path, three properties shape how the line itself looks. lineWidth is the stroke thickness in pixels (default 1). lineCap controls how the ends of an open line are drawn, and lineJoin controls how two connected segments meet at a corner.

lineCapEffect
butt (default)Square end, flush with the endpoint
roundSemicircle extending past the endpoint
squareSquare end extending past the endpoint by half the width
lineJoinEffect
miter (default)Sharp pointed corner
roundRounded corner
bevelFlattened, cut-off corner

The miterLimit property only matters when lineJoin is miter. At very sharp angles a miter can extend far beyond the join; miterLimit (default 10) is the maximum ratio of miter length to line width. Once that limit is exceeded, the corner falls back to a bevel automatically.

ctx.lineWidth = 12;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.miterLimit = 4;

ctx.beginPath();
ctx.moveTo(20, 80);
ctx.lineTo(70, 20);
ctx.lineTo(120, 80);
ctx.stroke();

Warning: A thin lineWidth of 1 straddling an integer coordinate looks blurry because it spans two physical pixels. Offset crisp 1px lines by 0.5 (e.g. moveTo(10.5, 0)) to align them to the pixel grid.

Try it: every style at once

This interactive demo combines fill, stroke, transparency, caps, joins, and a dashed line so you can see how each property changes the result. Open it in CodePen and tweak the numbers.

<canvas id="demo" width="320" height="200" style="border:1px solid #ccc"></canvas>
<script>
  const ctx = document.getElementById("demo").getContext("2d");

  // Translucent filled rectangle
  ctx.fillStyle = "rgba(59, 130, 246, 0.6)";
  ctx.fillRect(20, 20, 120, 80);

  // Thick rounded stroke with a sharp join
  ctx.strokeStyle = "#ef4444";
  ctx.lineWidth = 10;
  ctx.lineCap = "round";
  ctx.lineJoin = "round";
  ctx.beginPath();
  ctx.moveTo(180, 110);
  ctx.lineTo(230, 30);
  ctx.lineTo(290, 110);
  ctx.stroke();

  // Dashed baseline (see next section)
  ctx.setLineDash([12, 6]);
  ctx.lineWidth = 3;
  ctx.strokeStyle = "#111827";
  ctx.beginPath();
  ctx.moveTo(20, 160);
  ctx.lineTo(300, 160);
  ctx.stroke();
</script>

Dashed and dotted lines

Dashes are configured with setLineDash(segments), where segments is an array of on/off lengths in pixels. A single value like [5] produces a 5-on / 5-off dash; [12, 6] produces longer dashes with smaller gaps. The companion property lineDashOffset shifts where the pattern starts — animating it creates the classic “marching ants” selection effect.

ctx.setLineDash([15, 5]);   // 15px dash, 5px gap
ctx.lineDashOffset = 0;
ctx.strokeRect(10, 10, 100, 60);

ctx.setLineDash([]);        // reset to a solid line

Pass an empty array to setLineDash([]) to return to solid strokes. You can read the current pattern back with ctx.getLineDash().

<canvas id="ants" width="240" height="160" style="border:1px solid #ccc"></canvas>
<script>
  const ctx = document.getElementById("ants").getContext("2d");
  let offset = 0;

  function draw() {
    ctx.clearRect(0, 0, 240, 160);
    ctx.strokeStyle = "#2563eb";
    ctx.lineWidth = 2;
    ctx.setLineDash([8, 4]);
    ctx.lineDashOffset = -offset;        // negative makes ants crawl forward
    ctx.strokeRect(40, 30, 160, 100);
    offset = (offset + 1) % 12;          // wrap on the pattern length
    requestAnimationFrame(draw);
  }
  draw();
</script>

Reading styles back

Every style property is a normal getter, so you can inspect the current state at any time — handy for debugging or for restoring a value after a temporary change.

ctx.lineWidth = 4;
ctx.fillStyle = "#10b981";
console.log(ctx.lineWidth, ctx.fillStyle, ctx.globalAlpha);

Output:

4 #10b981 1

Note that the canvas normalizes color strings: a hex you set may read back as the same hex, while a named color or rgb() is often returned in a canonical form like "#10b981" or "rgba(...)".

Best Practices

  • Set every style property you depend on before drawing — never assume the context is in a default state, especially inside reused functions.
  • Always reset globalAlpha to 1 and call setLineDash([]) after a temporary effect, or wrap them in save() / restore() (see Save & restore state).
  • Prefer rgba() for one-off transparency and reserve globalAlpha for fading whole layers.
  • Use round caps and joins for soft, friendly UI strokes; keep miter for technical or pixel-precise diagrams.
  • Offset thin (1px) lines by 0.5 to keep them crisp on the pixel grid.
  • Animate lineDashOffset rather than rebuilding dash arrays each frame — it is far cheaper.
Last updated June 1, 2026
Was this helpful?