Interacting with web content using Chrome's new Side Panel extension feature.
Published: 2023-06-25 20:58:19

This post will explore Chrome's new Side Panel extension feature, including how you can use it with Content Scripts to create an interactive extension experience. 

With Chrome version 114 and above, developers can now utilize the Side Panel feature to display their extension page/application. [Neemi.ai](https://neemi.ai) has been updated to use it, and man is it smooth. Before its release, Neemi was manually altering the page to create a similar user experience, and it was quite cumbersome. Below, I will introduce the Side Panel and provide you with the tools to get started with a fully functional react/redux Side Panel application that communicates with the current tab and user interactions. Together we'll build a React app that tracks the user's cursor position and displays it in the side panel.


## Clone the repository and load the extension

First, go ahead a clone the repo [here](https://github.com/neemi-ai/chrome-extension-side-panel)

After you've cloned it, run the following commands in your terminal:
```
npm install
npm start
```
Next, open up your Chrome browser and make sure its version is up to date (if you see "update" in the top-right corner, you'll want to click that). After that, go ahead and navigate to chrome://extensions in a new tab.

If you don't already, enable developer mode in the top right corner, as seen below:



Then click "Load Unpacked" in the top left corner, find your repository directory, navigate to dist/chrome, and click "Select."



That's it, now you can click the extension icon and the side panel will open and display your current cursor position on the page. If you'd like an overview of [Neemi.ai](https://neemi.ai) using this feature with content scripts to create an interactive experience, keep reading. Or, if you just want to hack around with the public repository, then go for it!

### The sidePanel manifest v3 entry

Every Chrome extension is required to have a manifest file that determines what features the extension will utilize. For the side panel, in this tutorial, we use the action to open the side panel, so you'll need these two entries in your manifest file.
```
action: { default_title: 'Click to open panel' },
side_panel: { default_path: 'assets/app.html' }, 
```
[Neemi.ai](https://neemi.ai) uses scripting to build the manifest.json file, which you can find under config/manifest.

In our tutorial, we load the react app via the app.html file, which is loaded by the side panel. In the background script index.ts file you'll see the following code:
```
chrome.sidePanel
  .setPanelBehavior({ openPanelOnActionClick: true })
  .catch((error: any) => console.error(error));
```
### Loading the content scripts when the panel is open

Now getting the content scripts to run when the panel is open is the somewhat tricky part. In order to create a non-intrusive extension experience, [Neemi.ai](https://neemi.ai) wanted to make sure the app was only registering user mouse events when the side panel was open. Thus, we implemented a solution using Chrome's port messaging functionality.

First, the background script loads and listens for a port connection from the side panel and injects the content scripts
```
chrome.runtime.onConnect.addListener(port => {
  port.onMessage.addListener(async msg => {

    if (port.name === PortNames.SidePanelPort) {
      if (msg.type === 'init') {
        console.log('panel opened');

        await storage.setItem('panelOpen', true);

        port.onDisconnect.addListener(async () => {
          await storage.setItem('panelOpen', false);
          console.log('panel closed');
          console.log('port disconnected: ', port.name);
        });

        const tab = await getCurrentTab();

        if (!tab?.id) {
          console.error("Couldn't get current tab");
          return;
        }

        injectContentScript(tab.id);

        port.postMessage({ 
          type: 'handle-init', 
          message: 'panel open' 
        });
      }
    }
  });
});
```
Secondly, the background script listens for new tab connections and injects the content script only if the panel is open.
```
chrome.tabs.onUpdated
  .addListener(async (tabId, changeInfo, tab) => {
    if (!(tab.id && changeInfo.status === 'complete')) return;

    console.log('tab connected: ', tab.url, changeInfo);

    if (await storage.getItem('panelOpen')) {
      console.log('panel open');
      injectContentScript(tabId);
    }
  });
```
Notes
- This app uses webext-redux to create a redux store that seamlessly provides state updates between the background script and the Side Panel React application.
- The app uses typescript and webpack to create the application scripts, and all configurations can be found under the config directory.

If you want to learn more, play around with the repository to see how the side panel React application sends the 'init' port message upon rendering. Also, take a look at the webpack configuration if you want to see how the extension is built and javascript bundles are loaded to the side panel. Stay tuned for future blog posts that break down the application's components and packages. Thanks for reading!