Firefox workspaces proof of concept

Posted on Вс 08 марта 2026 in misc

Источник

I hacked together this CSS implementation of workspaces where a tab group gets promoted to a workspace by adding a < to its name. When all workspaces are collapsed you can see their labels but not their contents. Expand a workspace to reveal its contents (tabs and tab groups) and hide everything else. The current workspace sticks to the top so you can easily go back to the list.

A cool side effect is that you can search your workspaces by typing < in the address bar.

Caveats:

  • No support for horizontal tabs
  • No support for split view tabs
  • Moving tabs and tab groups between spaces is buggy
  • Spaces can't be re-ordered (you'd just move the head group but not the contents)
  • Set browser.tabs.insertAfterCurrent otherwise new tabs will always open in the very last workspace

Code: https://gist.github.com/entibo/3d9385890ab94a2a55453c9f523c94d3

I wouldn't recommend using this unless you know what you're doing.

/* For reference 

  #tabbrowser-arrowscrollbox > :is(tab, tab-group)

  tab-group > .tab-group-label-container + tab + tab

  tab-group[label^='<']                    Workspace
  tab-group[label^='<'][collapsed]         Collapsed workspace
  tab-group[label^='<']:not([collapsed])   Expanded workspace

  When there is an expanded workspace
  - hide everything before
  - hide everything starting at the next collapsed workspace
  - * except the active tab

  When there isn't an expanded workspace
  - show everything before the first workspace
  - show workspace group labels
  - show active tabs and tab-groups containing active tabs

  Tabs and tab group labels hidden using display: none; before the active tab 
  breaks drag-and-drop, so hide them using other properties (visibility, size)
*/

/* When there isn't an expanded workspace */

#tabbrowser-arrowscrollbox:not(:has(> tab-group[label^='<']:not([collapsed])))
  > tab-group[label^='<'] {
  /* &: Collapsed workspace */

  /* Hide the "+2" count of tabs in collapsed group */
  & ~ tab-group > .tab-group-overflow-count-container {
    display: none !important;
  }

  /* Hide normal tab groups when they don't contain the active tab  */
  &
    ~ tab-group:not([label^='<']):not([hasactivetab])
    > .tab-group-label-container {
    visibility: hidden !important;
    width: 0 !important;
    height: 0 !important;
    padding: 0 !important;
    margin: 0 !important;
  }

  /* Hide tabs when they're not active */
  & ~ tab:not([selected], [multiselected]) {
    visibility: hidden !important;
    width: 0 !important;
    height: 0 !important;
    padding: 0 !important;
  }
  & ~ tab-group > tab:not([selected], [multiselected]) {
    visibility: hidden !important;
    width: 0 !important;
    height: 0 !important;
    padding: 0 !important;
  }

  /* Indent visible tabs to indicate they belong to a workspace */
  & ~ tab {
    margin-inline-start: var(--space-small) !important;
  }
  & ~ tab-group:not([label^='<']) > * {
    margin-inline-start: calc(
      var(--space-small) + var(--space-medium)
    ) !important;
  }
}

/* When there is an expanded workspace */

/* Hide everything (with exceptions: active tab) */
#tabbrowser-arrowscrollbox:has(> tab-group[label^='<']:not([collapsed])) {
  & > :where(tab:not([selected])),
  & > tab-group > :where(tab:not([selected])),
  & > tab-group > :where(.tab-group-label-container) {
    display: none !important;
  }
}

#tabbrowser-arrowscrollbox > tab-group[label^='<']:not([collapsed]) {
  /* &: Expanded workspace */

  /* Show expanded workspace and its siblings */
  & > :is(tab, .tab-group-label-container),
  & ~ tab,
  & ~ tab-group > :where(tab:not([aria-hidden]), .tab-group-label-container) {
    display: flex !important;
  }

  & ~ tab-group[label^='<'][collapsed] {
    /* &: Next collapsed workspace */

    /* Hide everything */
    & > *,
    & ~ tab:not([selected]),
    & ~ tab-group > tab:not([selected]),
    & ~ tab-group > .tab-group-label-container {
      visibility: hidden !important;
      width: 0 !important;
      height: 0 !important;
      margin: 0 !important;
      padding: 0 !important;
    }

    /* Anchor the active tab to the top to ensure the workspace */
    /* opens scrolled to top (when active tab comes after the workspace) */
    & ~ tab[selected],
    & ~ tab-group > tab[selected] {
      order: -1;
    }
  }

  /* Pin the expanded workspace label */
  & > .tab-group-label-container {
    position: sticky !important;
    top: 0 !important;
    left: 0 !important;
    z-index: 100 !important; /* arbitrary */

    /* Replace the block-start margin with padding */
    margin-block-start: 0 !important;
    /* padding-block-start: var(--space-small) !important; */

    -margin-inline-start: 0;
    -padding-inline-start: var(--space-medium);
    --f: 12px;
    -margin-block-end: calc(-1 * var(--f));
    -padding-block-end: calc(var(--space-small) + var(--f));
  }
}

/* Re-style workspace tab-group labels */

#tabbrowser-arrowscrollbox > tab-group[label^='<'] .tab-group-label-container {
  /* (Vertical tabs) Make it full width */
  .tab-group-label {
    width: 100% !important;
  }
  tab-group[collapsed] > & .tab-group-label {
    margin-inline-end: 0 !important;
  }

  /* Replace bottom padding with margin so as not to catch pointer events */
  tab-group:not([collapsed]) > & {
    padding-block-end: 0 !important;
    margin-block-end: var(--space-small) !important;
  }

  /* Hide the stuff scrolling behind it by painting a border */
  tab-group:not([collapsed]) > & .tab-group-label {
    border: 2px solid var(--toolbox-bgcolor) !important;
    border-inline-start: 0 !important;
    border-block-end-width: 1px !important;
    margin-inline-start: -2px !important;
    margin-inline-end: calc(var(--space-medium) - 2px) !important;
    margin-block-start: -2px !important;
    box-sizing: content-box !important;
  }

  /* Hide the "<" prefix */
  .tab-group-label::first-letter {
    font-size: 0 !important;
  }

  /* We're going to use aboslute pseudo-elements */
  .tab-group-label {
    position: relative !important;

    /* ::before is an overlay to create a :hover effect */
    &::before {
      content: '';
      position: absolute;
      inset: 0;
    }
    &:hover::before {
      background-color: color-mix(in srgb, currentColor 10%, transparent);
    }
    &:active::before {
      background-color: color-mix(in srgb, currentColor 20%, transparent);
    }

    /* ::after renders a chevron icon */
    /* Its position and direction depend on whether the workspace is expanded */
    &::after {
      content: '';
      position: absolute;
      top: 0;
      bottom: 0;
      width: 24px;
      height: 100%;
      /* Using an svg icon as mask-image and coloring it using background-color */
      background-color: currentColor;
      mask-position: center center;
      mask-repeat: no-repeat;
    }
  }

  /* [ Collapsed workspace      > ] */
  tab-group[collapsed] > & .tab-group-label {
    text-align: left !important;
    padding-right: 20px !important;
    &::after {
      right: 0;
      mask-image: url(chrome://global/skin/icons/arrow-right.svg);
    }
  }

  /* [ <   Expanded workspace     ] */
  tab-group:not([collapsed]) > & .tab-group-label {
    text-align: center !important;
    padding-inline: 20px !important;
    &::after {
      left: 0;
      mask-image: url(chrome://global/skin/icons/arrow-left.svg);
    }
  }
}