In this tutorial, we'll cover how to take a screenshot in a Chrome extension and save it on the user's machine. This tutorial requires some beginner skills in Javascript.

We'll create an extension that allows the user to take a screenshot just by clicking the icon of the toolbar. The user can choose to take a screenshot of the entire screen, just a window, or the current tab.

Note that this extension will be using Manifest V3. I'll provide some hints about the differences between V3 and V2 throughout the tutorial, but if you want to know more about the differences between the two versions you can check out this tutorial.

You can find the code for this tutorial on this GitHub Repository.

Creating The Extension

We will not get into details on how to create a Chrome Extension, as it is not the purpose. If you need to learn more details about it you can check out this tutorial.

Create manifest.json in the root of your extension directory with the following content:

    "name": "Screenshots",
    "version": "0.0.1",
    "description": "Take screenshots",
    "manifest_version": 3,
    "action": {
        "default_title": "Take a Screenshot"
    "icons": {
        "16": "/assets/icon-16.png",
        "32": "/assets/icon-32.png",
        "48": "/assets/icon-48.png",
        "128": "/assets/icon-128.png"

The icons we are using for this extension are by BZZRICON Studio on Iconscout.

For Manifest V2, make sure the manifest_version is set to 2:

"manifest_version": 2

and make sure to replace action with browser_action:

"browser_action": {
	"default_title": "Take a Screenshot"

Then, create a zip of, go to chrome://extensions, enable Developer Mode from the top right if it isn't enabled, click "Load Unpacked" from the buttons on the left, and choose the directory of the extension. Our extension will be added successfully.

Add Service Worker (or Background Script)

To detect when a user clicks on the extension's icon, we need to attach an event listener to chrome.action.onClicked. To do that, we need to add a service worker (or background script for V2).

To add a service worker, add the following in manifest.json:

"background": {
	"service_worker": "background.js"

Or the following for V2:

"background": {
	"scripts": ["background.js"],
	"persistent": false

Next, create background.js in the root of the extension with the following content:

chrome.action.onClicked.addListener(function (tab) {

for V2 it should be the following:

chrome.browserAction.onClicked.addListener(function (tab) {

Note that if you don't have the action key in manifest.json, you will not be able to add a listener to onClicked.

Next, we'll start the "take screenshot" process. To do that, we'll use the Desktop Capture API. In particular, we'll use the method chrome.desktopCapture.chooseDesktopMedia which takes 3 parameters: The first is an array of strings of capture sources, which can be "screen", "window", "tab", and "audio". The second parameter is the target tab which is optional, however, in some cases if the target tab is not passed Chrome crashes. The third parameter is a callback that returns the stream id which we'll use later to get a screenshot.

add the following inside the listener:

    ], tab, (streamId) => {
        //check whether the user canceled the request or not
        if (streamId && streamId.length) {

Notice that we're passing in the first parameter "screen", "window", and "tab" as the allowed source types. The second parameter is the tab parameter passed to the listener, and the third is the callback function. We're checking if streamId is not empty since it will be empty if the user cancels the request.

Before we can use this though, we need to add some permissions in the manifest.json. Permissions allow the user to understand what the extension is doing and agree to it before it is installed in their browser.

Add the following to manifest.json:

"permissions": [

The reason we also need the tabs permission is because if we don't have the permission, the tab object passed to the onClicked event listener will not have the url parameter which is required for chooseDesktopMedia when passing that tab as a parameter.

So, if you reload the extension now and press the icon, you'll see that it will ask you what screen do you want to record and that's it. Next, we need to use the streamId to get the screenshot.

Add Content Script

To obtain the stream from the streamId, we need to use getUserMedia. However, this is not available in the service worker. So, we need to create a content script that receives a message from the service worker with the stream id, then gets the screenshot from the stream.

To add a content script, add the following to manifest.json:

"content_scripts": [
    	"matches": ["<all_urls>"],
    	"js": ["content_script.js"]

Then, create content_script.js in the root of the extension with the following content:

chrome.runtime.onMessage.addListener((message, sender, senderResponse) => {
    if ( === 'stream' && message.streamId) {

This code listens for the "onMessage" event and checks if the message received has a name property that's equal to stream and has a streamId property, then we'll obtain the stream and take a screenshot of it.

Inside the if, we'll use getUserMedia which returns a Promise that resolves to a MediaStream:

let track, canvas
    video: {
        mandatory: {
            chromeMediaSource: 'desktop',
            chromeMediaSourceId: message.streamId
}).then((stream) => {

Notice that the parameter we passed to getUserMedia takes an object of options. We're passing the chromeMediaSource which equals to desktop, and chromeMediaSourceId which equals to the stream Id we received.

Next, inside the callback function for the resolved promise, we'll get the MediaStreamTrack and then capture a screenshot from it using the ImageCapture API:

track = stream.getVideoTracks()[0]
const imageCapture = new ImageCapture(track)
return imageCapture.grabFrame()

In the end, we're returning the value of imageCapture.grabFrame which returns a Promise that resolves to an ImageBitmap. Notice that we didn't use the takePhoto method of the ImageCapture API. The reason behind that is that there are known cases of a DOMException thrown using it and this is a workaround for it.

Next, we'll attach another then method to handle the returned Promise from imageCapture.grabFrame. The callback function will stop the stream, create a canvas and draw the ImageBitmap in it, then get the Data Url of the canvas:

.then((bitmap) => {
    canvas = document.createElement('canvas');
    canvas.width = bitmap.width;
    canvas.height = bitmap.height;
    let context = canvas.getContext('2d');
    context.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height);
    return canvas.toDataURL();

Notice that it's important to set the width and height of the canvas to be equal to that of the bitmap. If we don't, the canvas height and width will default to 200px and if the width or height of the bitmap is larger than that then the screenshot will be cropped.

In the end, we're returning canvas.toDataUrl. We'll attach a last then method that takes the URL returned as a parameter. This URL will be used to download the image on the user's device:

.then((url) => {
    //TODO download the image from the URL
}).catch((err) => {
    alert("Could not take screenshot")
    senderResponse({success: false, message: err})

Notice that we also added catch to catch any errors. As you can see in the catch callback, we're calling the function senderResponse. This function is the one we'll pass from the service worker or background script to the content script when sending the message.

At the end of the if block we'll add the following:

return true;

In a onMessage event listener if the listener returns true that means that we'll later return a response to the sender using the callback function they passed when sending the message.

Download Screenshot

To download the screenshot, we'll use the Downloads API. It provides a lot of methods to manage downloads like search, open, remove, and more.

Before we can use any of the methods, we need to add the downloads permission to the permissions array in manifest.json:

"permissions": [

Now, we can use the methods of the Downloads API. We'll use the method which takes an array of options as the first parameter and a callback function as the second.

However, this method cannot be called from the content script. We need to call it from the service worker/background script. So, when we get to the TODO part in our code earlier, we need to send a message to the service worker with the URL we want to download.

To send a message in an extension, we use the chrome.runtime.sendMessage which takes as a first parameter the message to send (which can be of any type), and an optional callback function as the second parameter, which is the function that the receiver of the message should call to deliver the response.

Add the following code in place of the TODO comment:

.then((url) => {
    chrome.runtime.sendMessage({name: 'download', url}, (response) => {
        if (response.success) {
            alert("Screenshot saved");
        } else {
            alert("Could not save screenshot")
        senderResponse({success: true})

Notice that we're sending the message {name: 'download', url} to the receiver. As the message is sent to every listener in the extension, it's good to include a message property in the message you're sending to be able to handle different messages. We're also sending the URL to download the image from.

Let's go back to our service worker now. First, let's send a message to the content script from chooseDesktopMedia callback function we earlier did:

//check whether the user canceled the request or not
if (streamId && streamId.length) {
    setTimeout(() => {
        chrome.tabs.sendMessage(, {name: "stream", streamId}, (response) => console.log(response))
    }, 200)

Notice that to send a message to the content script we're using chrome.tabs.sendMessage. The difference between this one and chrome.runtime.sendMessage is that the former one sends the message to content scripts in a specific tab, whereas the first one sends the message to all scripts in the extension that listen to the onMessage handler.

Next, we'll add a listener to the onMessage event to receive the download message and download the file to the user's machine:

chrome.runtime.onMessage.addListener((message, sender, senderResponse) => {
    if ( === 'download' && message.url) {{
            filename: 'screenshot.png',
            url: message.url
        }, (downloadId) => {
            senderResponse({success: true})

        return true;

First, we're checking if the name property of the message is equal to download to make sure that the message received is the correct one. Then, we're downloading the file using, passing it the options object that has two options here: filename which is the name of the file to download, and url which is the URL to download. In the callback of the downloads method we're calling the callback function passed by the sender.

Our extension is now ready. Go to chrome://extensions again and reload the extension. Then, go to any page, click the icon of the extension. You'll be prompted to choose either the entire screen, a window, or a tab. Once you choose, a screenshot will be taken and saved on your machine.


In this tutorial, we learned how to a screenshot and a few of the concepts of a chrome extension shortly. If you want to learn more about Chrome extensions, make sure to check out the rest of my tutorials about browser extensions.