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);
}
}
}