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:
fillStyleandstrokeStylealso acceptCanvasGradientandCanvasPatternobjects, 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.
| Approach | Scope | Range | Typical use |
|---|---|---|---|
rgba() / hsla() alpha | One fill or stroke color | 0–1 | A single translucent shape |
globalAlpha | All drawing operations | 0–1 | Fading 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.
lineCap | Effect |
|---|---|
butt (default) | Square end, flush with the endpoint |
round | Semicircle extending past the endpoint |
square | Square end extending past the endpoint by half the width |
lineJoin | Effect |
|---|---|
miter (default) | Sharp pointed corner |
round | Rounded corner |
bevel | Flattened, 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
lineWidthof1straddling an integer coordinate looks blurry because it spans two physical pixels. Offset crisp 1px lines by0.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
globalAlphato1and callsetLineDash([])after a temporary effect, or wrap them insave()/restore()(see Save & restore state). - Prefer
rgba()for one-off transparency and reserveglobalAlphafor fading whole layers. - Use
roundcaps and joins for soft, friendly UI strokes; keepmiterfor technical or pixel-precise diagrams. - Offset thin (1px) lines by
0.5to keep them crisp on the pixel grid. - Animate
lineDashOffsetrather than rebuilding dash arrays each frame — it is far cheaper.