Menu

#987 Opening SidePanel with tabId Results in Global SidePanel

closed
nobody
None
2025-09-25
2023-07-21
Anonymous
No

Originally created by: eboraks

Chrome Version
Version 117.0.5901.0 (Official Build) canary (64-bit)

Use Case
This is about an extension that summarizes text from a webpage. When the user clicks the action button, the background script opens a side panel to display the summarized text. To accomplish this, I want the side panel to appear only after the action button is clicked and only for the tabs where the action button was clicked.

Describe the bug
When opening a side-panel and setting the option to use tabId, I expect that the sidePanel will be limited to the tab. But, that isn't the case.
According to the docs
When using sidePanel.open() (developer.chrome.com), you must choose the context in which it should open. Use windowId to open a global side panel. Alternatively, set the tabId to open the side panel only on a specific tab.

background.js

  console.log(
    'background.js got message. Action Clicked {tab.id: ' + tab.id + '}'
  )
  // This will open a tab-specific side panel only on the current tab.
  chrome.sidePanel.open({ tabId: tab.id })
  chrome.sidePanel.setOptions({
    tabId: tab.id,
    path: 'sidepanel/sidepanel.html',
    enabled: true,
  })
})

manifest

"background": {
    "service_worker": "src/background.js"
  },
  "side_panel": {
    "default_path": "sidepanel/sidepanel.html"
  },
  "permissions": ["tabs", "sidePanel"],

To Reproduce

  1. Clone this repo git@github.com:eboraks/learning-chrome-extension.git
  2. Upload extension
  3. Open two tabs
  4. Click the brain action button
  5. Navigate between the tabs.

Expected behavior
Since the tab was open with tabId the expected behavior is for the side panel to close when moving between tabs.

Discussion

  • Anonymous

    Anonymous - 2023-07-21

    Originally posted by: eboraks

    I added the following code to see the relationship between tabId and sidePanel

    chrome.tabs.onActivated.addListener(async ({ tabId }) => { const { path } = await chrome.sidePanel.getOptions({ tabId }) console.log('tabId: ' + tabId + ' path: ' + path) })

    As I moved between tabs that console printed the following.

    background.js:31 tabId: 1101905827 path: sidepanel/sidepanel.html
    background.js:31 tabId: 1101905829 path: sidepanel/sidepanel.html
    background.js:31 tabId: 1101905856 path: sidepanel/sidepanel.html

     
  • Anonymous

    Anonymous - 2023-08-23

    Originally posted by: zakariaelh

    hey there, were you able to resolve this? I'm having the same issue and it looks like the documentation says that this behavior is somehow expected.

    The tab in which to open the side panel. If the corresponding tab has a tab-specific side panel, the panel will only be open for that tab. If there is not a tab-specific panel, the global panel will be open in the specified tab and any other tabs without a currently-open tab- specific panel. This will override any currently-active side panel (global or tab-specific) in the corresponding tab. At least one of this and windowId must be provided.

    link

     
  • Anonymous

    Anonymous - 2023-08-23

    Originally posted by: AmySteam

    @eboraks Have you tried removing the default side panel declaration in the manifest?

      "side_panel": {
        "default_path": "sidepanel/sidepanel.html"
      },
    
     
  • Anonymous

    Anonymous - 2023-08-23

    Originally posted by: zakariaelh

    This works, thank you @AmySteam!

     
  • Anonymous

    Anonymous - 2023-08-23

    Ticket changed by: AmySteam

    • status: open --> closed
     
  • Anonymous

    Anonymous - 2023-11-02

    Originally posted by: ehynds

    For other users who might land here, if you have code that looks similar to this:

    await chrome.sidePanel.setOptions({
      tabId,
      path: 'panel.html',
      enabled: true,
    });
    
    await chrome.sidePanel.open({
      tabId,
    });
    

    And are receiving this error:

    Uncaught (in promise) Error: No active side panel for tabId
    

    Make sure you're calling setOptions before open. After making that change, you might start experiencing this error:

    Error: `sidePanel.open()` may only be called in response to a user gesture
    

    To fix, remove await from the setOptions call. Final working code (for me anyway):

    chrome.sidePanel.setOptions({
      tabId,
      path: 'panel.html',
      enabled: true,
    });
    
    await chrome.sidePanel.open({
      tabId,
    });
    
     
  • Anonymous

    Anonymous - 2024-06-10

    Originally posted by: bmz1

    @ehynds Thanks, it's indeed working. However, the type definitions seem a bit off for open(), as it indicates that it's not a promise.

     
  • Anonymous

    Anonymous - 2024-06-10

    Originally posted by: belthaZornv

    chrome.sidePanel.setOptions({
    tabId,
    path: 'panel.html',
    enabled: true,
    });

    await chrome.sidePanel.open({
    tabId,
    });

    If I do that, this won't work:

      // Get the currently active tab
      const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true });
      const activeTab = activeTabs[0];
    
      // Check if there is an active tab
      if (!activeTab) {
        console.error('No active tab found.');
        return;
      }
    
      // Disable side panel for all tabs
      const allTabs = await chrome.tabs.query({});
      for (const tab of allTabs) {
        await chrome.sidePanel.setOptions({
          tabId: tab.id,
          enabled: false
        });
      }
    
      // Enable side panel for the active tab
      await chrome.sidePanel.setOptions({
        tabId: activeTab.id,
        path: 'sidebar.html',
        enabled: true
      });
    });
    

    Still surprised how complicated it is to just set a sidepanel to stay active on the initial tab 🤦‍♂️

     
  • Anonymous

    Anonymous - 2024-06-10

    Originally posted by: belthaZornv

    Tried this too:

    chrome.action.onClicked.addListener(async (tab) => {
      // Get the currently active tab
        const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true });
        const activeTab = activeTabs[0];
    
        chrome.sidePanel.setOptions({
          activeTab.id,
          path: 'sidebar.html',
          enabled: true,
        });
    
        await chrome.sidePanel.open({
          activeTab.id,
        });
    });
    

    To no avail, @ehynds do you see anything wrong from what I'm trying to achieve? :D

     
  • Anonymous

    Anonymous - 2024-06-10

    Originally posted by: oliverdunk

    @belthaZornv, you should be able to use tab.id (from the event listener arguments) rather than querying for the active tab explicitly :)

    That will also allow you to remove the await and call open synchronously.

     
  • Anonymous

    Anonymous - 2024-06-10

    Originally posted by: belthaZornv

    @belthaZornv, you should be able to use tab.id (from the event listener arguments) rather than querying for the active tab explicitly :)

    That will also allow you to remove the await and call open synchronously.

    oops, what a blonde moment that is. hahaha! let me try with that!

     
  • Anonymous

    Anonymous - 2024-06-10

    Originally posted by: belthaZornv

    Cleaned up the code - still opening on all tabs though lol!

    chrome.action.onClicked.addListener(async (tab) => {
        chrome.sidePanel.setOptions({
          tab.id,
          path: 'sidebar.html',
          enabled: true,
        });
    
        chrome.sidePanel.open({
          tab.id,
        });
    });
    
     
  • Anonymous

    Anonymous - 2024-06-10

    Originally posted by: oliverdunk

    still opening on all tabs though lol

    Yeah, this seems to be the behavior today. I don't remember off the top of my head if that was intentional - I'll try to follow-up on https://github.com/GoogleChrome/chrome-extensions-samples/issues/1179.

     
  • Anonymous

    Anonymous - 2024-06-10

    Originally posted by: belthaZornv

    Thanks @oliverdunk, appreciate it 🙏

     
  • Anonymous

    Anonymous - 2024-06-15

    Originally posted by: onslauth

    @belthaZornv

    I have it working using this

    chrome.action.onClicked.addListener( ( tab ) => {
    
      chrome.sidePanel.setOptions( { 
          tabId: tab.id,
          path: "sidepanel.html",
          enabled: true 
      } );
      chrome.sidePanel.open( { tabId: tab.id } );
    
    } );
    
     
  • Anonymous

    Anonymous - 2024-10-31

    Originally posted by: Mohamed3nan

    Wow, I can't believe how long it took to figure this out! 😄
    Thank you!

    --

    Here's what finally worked for me after spending the whole day on it :D

    function openSidePanel(windowId, tabId, scope) {
        const sidePanelPath = 'sidepanel/sidepanel.htmll';
        const options = {
            path: sidePanelPath,
            enabled: true,
            ...(tabId !== undefined ? { tabId } : { windowId })
        };
    
        if (scope === 'tab-specific' && tabId !== undefined) {
            chrome.sidePanel.setOptions(options, () => {
                chrome.sidePanel.open({ tabId });
            });
        } else {
            chrome.sidePanel.setOptions({ path: sidePanelPath, enabled: true }, () => {
                chrome.sidePanel.open({ windowId });
            });
        }
    }
    

    and in the Manifest file remove this:

      "side_panel": {
        "default_path": "sidepanel/sidepanel.html"
      },
      ```
    
      finally, if you made any edits to the scope you must reload the extension
    
     
  • Anonymous

    Anonymous - 2024-11-16

    Originally posted by: munr0

    @Mohamed3nan, this workaround worked for me! It seems like the issue comes down to the thing in the manifest.

    unfortunately however using .setOptions before .open to emulate this manifest declaration takes too long when combined with my other code and now I'm running into this...

    CC: [#1179]

     

    Related

    Tickets: #1179

  • Anonymous

    Anonymous - 2025-09-25

    Originally posted by: Nabeel-Asghar

    Default path should be removable as a cli command.

    This is the way I got it to work on only certain sites I wanted it to and I have to manually remove "side_panel": { "default_path": "sidepanel.html" } from the manifest.json that's generated.

    const site_origin = "https://google.com";
    
    chrome.tabs.onUpdated.addListener(async (tabId, info, tab) => {
      if (!tab.url) return;
    
      const url = new URL(tab.url);
    
      if (
        url.origin === site_origin
      ) {
        await chrome.sidePanel.setOptions({
          tabId,
          path: "side_panel.html",
          enabled: true,
        });
      } else {
        // Disable everywhere else
        await chrome.sidePanel.setOptions({
          tabId,
          enabled: false,
        });
      }
    });
    
    // To avoid having to right click and "Open Side Panel"
    chrome.action.onClicked.addListener(async (tab) => {
      await chrome.sidePanel.open({ tabId: tab.id });
    });
    
     

Log in to post a comment.