
Before you paint anything on the screen, you need to tell the browser where it’s going to happen. This is done by grabbing a reference to the canvas element from the DOM, then asking it for its drawing context. The context is essentially your bucket of paint – without it, you can’t begin to draw.
Here’s the basic pattern:
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
That ctx object is your gateway to everything you can draw: lines, shapes, images, text, gradients – basically anything that fits inside a rectangle.
One subtlety here is that if you forget to get the context, or if you try for something other than 2D (say, ‘webgl’ without setting up WebGL properly), you’ll end up with null. This causes errors down the line that aren’t always obvious – so always check:
if (!ctx) {
throw new Error('Failed to get 2D context');
}
Also, the size of your canvas element matters. The width and height attributes on the canvas tag define the drawing surface size, not the CSS width and height. If you set the CSS size without adjusting the canvas attributes, you’ll get scaling artifacts.
Example:
<canvas id="myCanvas" width="800" height="600"></canvas>
If you only set style="width: 800px; height: 600px;" but leave the attributes at their default (300×150), your drawing surface is still 300 by 150 pixels internally, and the browser stretches it, which looks blurry.
So, always set the canvas size attributes to the actual pixel dimensions you intend to draw at, and then you can scale the canvas element with CSS if you want to handle high-DPI displays manually. This is crucial for sharp-looking graphics.
Ailun Screen Protector for iPad 11th A16 2025 [11 Inch] / 10th Generation 2022 [10.9 Inch], Tempered Glass [Face ID & Apple Pencil Compatible] Ultra Sensitive Case Friendly [2 Pack]
$7.98 (as of June 3, 2026 23:09 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Now you just have to tell it what to paint
With the context object in hand, you’re ready to draw. The canvas API is built around the idea of paths. You first define a shape or a line by creating a path, and then you tell the browser what to do with that path-either draw its outline (stroke) or fill it in (fill). It’s a two-step process: define, then render.
Let’s start with the simplest thing possible: a straight line. You use moveTo(x, y) to position your virtual pen, and then lineTo(x, y) to trace a line from that starting point to a new one.
// Move the "pen" to the starting coordinates without drawing ctx.moveTo(50, 50); // Draw a line to the ending coordinates ctx.lineTo(250, 150); // Nothing appears on screen yet. Now, render the path we just defined. ctx.stroke();
Notice that nothing happens until you call stroke(). You can define a complex path with hundreds of points, but until you call stroke() or fill(), the canvas remains stubbornly blank. This is a common source of confusion; you’ve written all the drawing code, but you forgot the final command to actually put the pixels on the screen.
For shapes, you can draw them line by line, or you can use built-in helper functions. The rect(x, y, width, height) function is a classic. It defines a rectangular path for you. Once defined, you can choose to fill it, stroke it, or both.
// Set the color for the inside of the shape ctx.fillStyle = 'rgba(0, 0, 255, 0.5)'; // A semi-transparent blue // Set the style for the border ctx.strokeStyle = 'black'; ctx.lineWidth = 4; // Define the rectangle's path ctx.rect(100, 100, 300, 200); // Fill the rectangle with the current fillStyle ctx.fill(); // Draw the border with the current strokeStyle and lineWidth ctx.stroke();
The context is a state machine. Properties like fillStyle, strokeStyle, and lineWidth are part of its current state. When you set ctx.fillStyle = 'blue', every subsequent call to fill() will use blue paint until you change the state again. This is efficient, but it’s also a trap. If you draw a blue rectangle and then forget to change the fillStyle before drawing your next shape, that shape will also be blue, which is probably not what you intended. You have to meticulously manage the state before each drawing operation.
The one big thing you’ll forget and why it will drive you crazy
So you’ve drawn your first shape, maybe a nice red rectangle. You feel a sense of accomplishment. Now, you want to draw a second shape, a blue line, somewhere else on the canvas. This seems easy enough. You just set the strokeStyle to blue, call moveTo and lineTo, and then stroke. You run the code, and your jaw drops. Not only is your new blue line there, but your original red rectangle now has a blue outline, and there’s a bizarre blue line connecting the corner of the rectangle to the start of your new line.
What on earth is going on? You check your code a dozen times. You set the color for the rectangle, you drew it. You set the color for the line, you drew it. The logic seems flawless. This, my friend, is the trap. The problem isn’t your logic; it’s that you forgot to tell the canvas context that you were finished with the first shape and starting a new one.
The one function you will forget, over and over, is beginPath(). Without it, the canvas context thinks every drawing command you issue is part of one single, gigantic, continuous path. When you call stroke() or fill(), it re-draws *everything* in that ever-growing path with the current styles.
Here’s the broken code. It’s trying to draw a filled red rectangle and then a separate blue line.
// Draw a red rectangle ctx.fillStyle = 'red'; ctx.rect(10, 10, 100, 100); ctx.fill(); // Now, try to draw a blue line ctx.strokeStyle = 'blue'; ctx.moveTo(150, 50); ctx.lineTo(250, 150); ctx.stroke(); // This will re-stroke the rectangle too!
When the final stroke() is called, the path contains the rectangle from ctx.rect() AND the new line from moveTo/lineTo. The browser obediently strokes the entire thing in blue, creating a mess. The fix is to explicitly tell the browser to start a new path before you define the new shape.
// Draw a red rectangle ctx.beginPath(); // Start path for the rectangle ctx.fillStyle = 'red'; ctx.rect(10, 10, 100, 100); ctx.fill(); // Now, draw a blue line ctx.beginPath(); // <-- The crucial, forgotten line. Start a NEW path. ctx.strokeStyle = 'blue'; ctx.moveTo(150, 50); ctx.lineTo(250, 150); ctx.stroke();
By calling beginPath(), you essentially throw away the old path definition (the rectangle is already drawn, so we don't need its path anymore) and start with a clean slate. Now, when you call stroke(), the only thing in the current path is the line you just defined. No more weird phantom shapes. You will forget this. It will drive you crazy. And then you will remember this rule: always call beginPath() before you start drawing a new thing.
This is different from ctx.save() and ctx.restore(). Those functions manage the entire state of the context-colors, line widths, transformations, clipping regions, everything-but they do *not* affect the current path. You use save() and restore() when you want to temporarily change styles, draw something, and then pop back to the previous styles. You use beginPath() to start a new shape. You will often use them together.
