├── .gitignore ├── LICENSE.txt ├── README.md ├── bower.json ├── chrome-tabs.gif ├── chrome-tabs.mov ├── css ├── chrome-tabs-dark-theme.css ├── chrome-tabs-dark-theme.styl ├── chrome-tabs.css └── chrome-tabs.styl ├── demo ├── css │ └── demo.css └── images │ ├── facebook-favicon.ico │ └── google-favicon.ico ├── index.html ├── js └── chrome-tabs.js ├── older-versions.md ├── package.json └── svg ├── tab.sketch └── tab.svg /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | bower_components/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2013 Adam Schwartz 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Chrome Tabs in Chrome 2 | 3 | Exactly what you think this is. Go wild. 4 | 5 | Drag-and-drop support provided by [Draggabilly](https://github.com/desandro/draggabilly) by @desandro. 6 | 7 | ### [Live demo](http://adamschwartz.co/chrome-tabs/) 8 | 9 | 10 | 11 |
12 | 13 | [Older versions](older-versions.md) 14 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-tabs", 3 | "homepage": "https://github.com/adamschwartz/chrome-tabs", 4 | "authors": [ 5 | "Adam Schwartz" 6 | ], 7 | "description": "Chrome-style tabs in HTML/CSS/JS", 8 | "main": "js/chrome-tabs.js", 9 | "keywords": [ 10 | "chrome", 11 | "tabs", 12 | "html" 13 | ], 14 | "license": "MIT", 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "bower_components", 19 | "test", 20 | "tests" 21 | ], 22 | "dependencies": { 23 | "draggabilly": "2.1.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /chrome-tabs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamschwartz/chrome-tabs/1046a3fd4d164bc3550d82dc46ff1f13c6438e2f/chrome-tabs.gif -------------------------------------------------------------------------------- /chrome-tabs.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamschwartz/chrome-tabs/1046a3fd4d164bc3550d82dc46ff1f13c6438e2f/chrome-tabs.mov -------------------------------------------------------------------------------- /css/chrome-tabs-dark-theme.css: -------------------------------------------------------------------------------- 1 | .chrome-tabs.chrome-tabs-dark-theme { 2 | background: #202124; 3 | } 4 | .chrome-tabs.chrome-tabs-dark-theme .chrome-tab .chrome-tab-dividers::before, 5 | .chrome-tabs.chrome-tabs-dark-theme .chrome-tab .chrome-tab-dividers::after { 6 | background: #4a4d51; 7 | } 8 | .chrome-tabs.chrome-tabs-dark-theme .chrome-tab .chrome-tab-background > svg .chrome-tab-geometry { 9 | fill: #292b2e; 10 | } 11 | .chrome-tabs.chrome-tabs-dark-theme .chrome-tab[active] .chrome-tab-background > svg .chrome-tab-geometry { 12 | fill: #323639; 13 | } 14 | .chrome-tabs.chrome-tabs-dark-theme .chrome-tab .chrome-tab-title { 15 | color: #9ca1a7; 16 | } 17 | .chrome-tabs.chrome-tabs-dark-theme .chrome-tab[active] .chrome-tab-title { 18 | color: #f1f3f4; 19 | } 20 | .chrome-tabs.chrome-tabs-dark-theme .chrome-tab .chrome-tab-close { 21 | background-image: url("data:image/svg+xml;utf8,"); 22 | } 23 | .chrome-tabs.chrome-tabs-dark-theme .chrome-tab .chrome-tab-close:hover { 24 | background-color: #5f6368; 25 | background-image: url("data:image/svg+xml;utf8,"); 26 | } 27 | .chrome-tabs.chrome-tabs-dark-theme .chrome-tab .chrome-tab-close:hover:active { 28 | background-color: #80868b; 29 | background-image: url("data:image/svg+xml;utf8,"); 30 | } 31 | .chrome-tabs.chrome-tabs-dark-theme .chrome-tabs-bottom-bar { 32 | background: #323639; 33 | } 34 | -------------------------------------------------------------------------------- /css/chrome-tabs-dark-theme.styl: -------------------------------------------------------------------------------- 1 | dividersBackgroundColor = #4a4d51 2 | activeTabBackgroundColor = #323639 3 | 4 | .chrome-tabs.chrome-tabs-dark-theme 5 | background #202124 6 | 7 | .chrome-tab 8 | 9 | .chrome-tab-dividers 10 | 11 | &::before, &::after 12 | background #4a4d51 13 | 14 | .chrome-tab-background > svg .chrome-tab-geometry 15 | fill #292b2e 16 | 17 | &[active] .chrome-tab-background > svg .chrome-tab-geometry 18 | fill activeTabBackgroundColor 19 | 20 | .chrome-tab-title 21 | color #9ca1a7 22 | 23 | &[active] .chrome-tab-title 24 | color #f1f3f4 25 | 26 | .chrome-tab-close 27 | background-image url("data:image/svg+xml;utf8,") 28 | 29 | &:hover 30 | background-color #5f6368 31 | background-image url("data:image/svg+xml;utf8,") 32 | 33 | &:active 34 | background-color #80868b 35 | background-image url("data:image/svg+xml;utf8,") 36 | 37 | .chrome-tabs-bottom-bar 38 | background activeTabBackgroundColor 39 | -------------------------------------------------------------------------------- /css/chrome-tabs.css: -------------------------------------------------------------------------------- 1 | .chrome-tabs { 2 | box-sizing: border-box; 3 | position: relative; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 5 | font-size: 12px; 6 | height: 46px; 7 | padding: 8px 3px 4px 3px; 8 | background: #dee1e6; 9 | border-radius: 5px 5px 0 0; 10 | overflow: hidden; 11 | } 12 | .chrome-tabs * { 13 | box-sizing: inherit; 14 | font: inherit; 15 | } 16 | .chrome-tabs .chrome-tabs-content { 17 | position: relative; 18 | width: 100%; 19 | height: 100%; 20 | } 21 | .chrome-tabs .chrome-tab { 22 | position: absolute; 23 | left: 0; 24 | height: 36px; 25 | width: 240px; 26 | border: 0; 27 | margin: 0; 28 | z-index: 1; 29 | pointer-events: none; 30 | } 31 | .chrome-tabs .chrome-tab, 32 | .chrome-tabs .chrome-tab * { 33 | user-select: none; 34 | cursor: default; 35 | } 36 | .chrome-tabs .chrome-tab .chrome-tab-dividers { 37 | position: absolute; 38 | top: 7px; 39 | bottom: 7px; 40 | left: var(--tab-content-margin); 41 | right: var(--tab-content-margin); 42 | } 43 | .chrome-tabs .chrome-tab .chrome-tab-dividers, 44 | .chrome-tabs .chrome-tab .chrome-tab-dividers::before, 45 | .chrome-tabs .chrome-tab .chrome-tab-dividers::after { 46 | pointer-events: none; 47 | } 48 | .chrome-tabs .chrome-tab .chrome-tab-dividers::before, 49 | .chrome-tabs .chrome-tab .chrome-tab-dividers::after { 50 | content: ""; 51 | display: block; 52 | position: absolute; 53 | top: 0; 54 | bottom: 0; 55 | width: 1px; 56 | background: #a9adb0; 57 | opacity: 1; 58 | transition: opacity 0.2s ease; 59 | } 60 | .chrome-tabs .chrome-tab .chrome-tab-dividers::before { 61 | left: 0; 62 | } 63 | .chrome-tabs .chrome-tab .chrome-tab-dividers::after { 64 | right: 0; 65 | } 66 | .chrome-tabs .chrome-tab:first-child .chrome-tab-dividers::before, 67 | .chrome-tabs .chrome-tab:last-child .chrome-tab-dividers::after { 68 | opacity: 0; 69 | } 70 | .chrome-tabs .chrome-tab .chrome-tab-background { 71 | position: absolute; 72 | top: 0; 73 | left: 0; 74 | width: 100%; 75 | height: 100%; 76 | overflow: hidden; 77 | pointer-events: none; 78 | } 79 | .chrome-tabs .chrome-tab .chrome-tab-background > svg { 80 | width: 100%; 81 | height: 100%; 82 | } 83 | .chrome-tabs .chrome-tab .chrome-tab-background > svg .chrome-tab-geometry { 84 | fill: #f4f5f6; 85 | } 86 | .chrome-tabs .chrome-tab[active] { 87 | z-index: 5; 88 | } 89 | .chrome-tabs .chrome-tab[active] .chrome-tab-background > svg .chrome-tab-geometry { 90 | fill: #fff; 91 | } 92 | .chrome-tabs .chrome-tab:not([active]) .chrome-tab-background { 93 | transition: opacity 0.2s ease; 94 | opacity: 0; 95 | } 96 | @media (hover: hover) { 97 | .chrome-tabs .chrome-tab:not([active]):hover { 98 | z-index: 2; 99 | } 100 | .chrome-tabs .chrome-tab:not([active]):hover .chrome-tab-background { 101 | opacity: 1; 102 | } 103 | } 104 | .chrome-tabs .chrome-tab.chrome-tab-was-just-added { 105 | top: 10px; 106 | animation: chrome-tab-was-just-added 120ms forwards ease-in-out; 107 | } 108 | .chrome-tabs .chrome-tab .chrome-tab-content { 109 | position: absolute; 110 | display: flex; 111 | top: 0; 112 | bottom: 0; 113 | left: var(--tab-content-margin); 114 | right: var(--tab-content-margin); 115 | padding: 9px 8px; 116 | border-top-left-radius: 8px; 117 | border-top-right-radius: 8px; 118 | overflow: hidden; 119 | pointer-events: all; 120 | } 121 | .chrome-tabs .chrome-tab[is-mini] .chrome-tab-content { 122 | padding-left: 2px; 123 | padding-right: 2px; 124 | } 125 | .chrome-tabs .chrome-tab .chrome-tab-favicon { 126 | position: relative; 127 | flex-shrink: 0; 128 | flex-grow: 0; 129 | height: 16px; 130 | width: 16px; 131 | background-size: 16px; 132 | margin-left: 4px; 133 | } 134 | .chrome-tabs .chrome-tab[is-small] .chrome-tab-favicon { 135 | margin-left: 0; 136 | } 137 | .chrome-tabs .chrome-tab[is-mini]:not([active]) .chrome-tab-favicon { 138 | margin-left: auto; 139 | margin-right: auto; 140 | } 141 | .chrome-tabs .chrome-tab[is-mini][active] .chrome-tab-favicon { 142 | display: none; 143 | } 144 | .chrome-tabs .chrome-tab .chrome-tab-title { 145 | flex: 1; 146 | vertical-align: top; 147 | overflow: hidden; 148 | white-space: nowrap; 149 | margin-left: 4px; 150 | color: #5f6368; 151 | -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 24px), transparent); 152 | mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 24px), transparent); 153 | } 154 | .chrome-tabs .chrome-tab[is-small] .chrome-tab-title { 155 | margin-left: 0; 156 | } 157 | .chrome-tabs .chrome-tab .chrome-tab-favicon + .chrome-tab-title, 158 | .chrome-tabs .chrome-tab[is-small] .chrome-tab-favicon + .chrome-tab-title { 159 | margin-left: 8px; 160 | } 161 | .chrome-tabs .chrome-tab[is-smaller] .chrome-tab-favicon + .chrome-tab-title, 162 | .chrome-tabs .chrome-tab[is-mini] .chrome-tab-title { 163 | display: none; 164 | } 165 | .chrome-tabs .chrome-tab[active] .chrome-tab-title { 166 | color: #45474a; 167 | } 168 | .chrome-tabs .chrome-tab .chrome-tab-drag-handle { 169 | position: absolute; 170 | top: 0; 171 | bottom: 0; 172 | right: 0; 173 | left: 0; 174 | border-top-left-radius: 8px; 175 | border-top-right-radius: 8px; 176 | } 177 | .chrome-tabs .chrome-tab .chrome-tab-close { 178 | flex-grow: 0; 179 | flex-shrink: 0; 180 | position: relative; 181 | width: 16px; 182 | height: 16px; 183 | border-radius: 50%; 184 | background-image: url("data:image/svg+xml;utf8,"); 185 | background-position: center center; 186 | background-repeat: no-repeat; 187 | background-size: 8px 8px; 188 | } 189 | @media (hover: hover) { 190 | .chrome-tabs .chrome-tab .chrome-tab-close:hover { 191 | background-color: #e8eaed; 192 | } 193 | .chrome-tabs .chrome-tab .chrome-tab-close:hover:active { 194 | background-color: #dadce0; 195 | } 196 | } 197 | @media not all and (hover: hover) { 198 | .chrome-tabs .chrome-tab .chrome-tab-close:active { 199 | background-color: #dadce0; 200 | } 201 | } 202 | @media (hover: hover) { 203 | .chrome-tabs .chrome-tab:not([active]) .chrome-tab-close:not(:hover):not(:active) { 204 | opacity: 0.8; 205 | } 206 | } 207 | .chrome-tabs .chrome-tab[is-smaller] .chrome-tab-close { 208 | margin-left: auto; 209 | } 210 | .chrome-tabs .chrome-tab[is-mini]:not([active]) .chrome-tab-close { 211 | display: none; 212 | } 213 | .chrome-tabs .chrome-tab[is-mini][active] .chrome-tab-close { 214 | margin-left: auto; 215 | margin-right: auto; 216 | } 217 | @-moz-keyframes chrome-tab-was-just-added { 218 | to { 219 | top: 0; 220 | } 221 | } 222 | @-webkit-keyframes chrome-tab-was-just-added { 223 | to { 224 | top: 0; 225 | } 226 | } 227 | @-o-keyframes chrome-tab-was-just-added { 228 | to { 229 | top: 0; 230 | } 231 | } 232 | @keyframes chrome-tab-was-just-added { 233 | to { 234 | top: 0; 235 | } 236 | } 237 | .chrome-tabs.chrome-tabs-is-sorting .chrome-tab:not(.chrome-tab-is-dragging), 238 | .chrome-tabs:not(.chrome-tabs-is-sorting) .chrome-tab.chrome-tab-was-just-dragged { 239 | transition: transform 120ms ease-in-out; 240 | } 241 | .chrome-tabs .chrome-tabs-bottom-bar { 242 | position: absolute; 243 | bottom: 0; 244 | height: 4px; 245 | left: 0; 246 | width: 100%; 247 | background: #fff; 248 | z-index: 10; 249 | } 250 | .chrome-tabs-optional-shadow-below-bottom-bar { 251 | position: relative; 252 | height: 1px; 253 | width: 100%; 254 | background-image: url("data:image/svg+xml;utf8,"); 255 | background-size: 1px 1px; 256 | background-repeat: repeat-x; 257 | background-position: 0% 0%; 258 | } 259 | @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { 260 | .chrome-tabs-optional-shadow-below-bottom-bar { 261 | background-image: url("data:image/svg+xml;utf8,"); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /css/chrome-tabs.styl: -------------------------------------------------------------------------------- 1 | chromeTabSpaceAbove = 8px 2 | chromeTabSpaceBelow = 4px 3 | activeTabBackgroundColor = #fff 4 | 5 | .chrome-tabs 6 | box-sizing border-box 7 | position relative 8 | font-family -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" 9 | font-size 12px 10 | height 46px 11 | padding chromeTabSpaceAbove 3px chromeTabSpaceBelow 3px 12 | background #dee1e6 13 | border-radius 5px 5px 0 0 14 | overflow hidden 15 | 16 | * 17 | box-sizing inherit 18 | font inherit 19 | 20 | .chrome-tabs-content 21 | position relative 22 | width 100% 23 | height 100% 24 | 25 | .chrome-tab 26 | position absolute 27 | left 0 28 | height 36px 29 | width 240px 30 | border 0 31 | margin 0 32 | z-index 1 33 | pointer-events none 34 | 35 | &, * 36 | user-select none 37 | cursor default 38 | 39 | .chrome-tab-dividers 40 | position absolute 41 | top 7px // TODO - check these are right 42 | bottom 7px 43 | left var(--tab-content-margin) 44 | right var(--tab-content-margin) 45 | 46 | &, &::before, &::after 47 | pointer-events none 48 | 49 | &::before, &::after 50 | content "" 51 | display block 52 | position absolute 53 | top 0 54 | bottom 0 55 | width 1px 56 | background #a9adb0 57 | opacity 1 58 | transition opacity .2s ease 59 | 60 | &::before 61 | left 0 62 | 63 | &::after 64 | right 0 65 | 66 | &:first-child .chrome-tab-dividers::before 67 | &:last-child .chrome-tab-dividers::after 68 | opacity 0 69 | 70 | .chrome-tab-background 71 | position absolute 72 | top 0 73 | left 0 74 | width 100% 75 | height 100% 76 | overflow hidden 77 | pointer-events none 78 | 79 | > svg 80 | width 100% 81 | height 100% 82 | 83 | .chrome-tab-geometry 84 | fill #f4f5f6 85 | 86 | &[active] 87 | z-index 5 88 | 89 | .chrome-tab-background > svg .chrome-tab-geometry 90 | fill activeTabBackgroundColor 91 | 92 | &:not([active]) 93 | 94 | .chrome-tab-background 95 | transition opacity .2s ease 96 | opacity 0 97 | 98 | @media (hover: hover) 99 | &:hover 100 | z-index 2 101 | 102 | .chrome-tab-background 103 | opacity 1 104 | 105 | @keyframes chrome-tab-was-just-added 106 | to 107 | top 0 108 | 109 | &.chrome-tab-was-just-added 110 | top 10px 111 | animation chrome-tab-was-just-added 120ms forwards ease-in-out 112 | 113 | .chrome-tab-content 114 | position absolute 115 | display flex 116 | top 0 117 | bottom 0 118 | left var(--tab-content-margin) 119 | right var(--tab-content-margin) 120 | padding 9px 8px 121 | border-top-left-radius 8px 122 | border-top-right-radius 8px 123 | overflow hidden 124 | pointer-events all 125 | 126 | &[is-mini] .chrome-tab-content 127 | padding-left 2px 128 | padding-right 2px 129 | 130 | .chrome-tab-favicon 131 | position relative 132 | flex-shrink 0 133 | flex-grow 0 134 | height 16px 135 | width 16px 136 | background-size 16px 137 | margin-left 4px 138 | 139 | &[is-small] .chrome-tab-favicon 140 | margin-left 0 141 | 142 | &[is-mini]:not([active]) .chrome-tab-favicon 143 | margin-left auto 144 | margin-right auto 145 | 146 | &[is-mini][active] .chrome-tab-favicon 147 | display none 148 | 149 | .chrome-tab-title 150 | flex 1 151 | vertical-align top 152 | overflow hidden 153 | white-space nowrap 154 | margin-left 4px 155 | color #5f6368 156 | -webkit-mask-image linear-gradient(90deg, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 1) calc(100% - 24px), transparent) 157 | mask-image linear-gradient(90deg, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 1) calc(100% - 24px), transparent) 158 | 159 | &[is-small] .chrome-tab-title 160 | margin-left 0 161 | 162 | .chrome-tab-favicon + .chrome-tab-title 163 | &[is-small] .chrome-tab-favicon + .chrome-tab-title 164 | margin-left 8px 165 | 166 | &[is-smaller] .chrome-tab-favicon + .chrome-tab-title 167 | &[is-mini] .chrome-tab-title 168 | display none 169 | 170 | &[active] .chrome-tab-title 171 | color #45474a 172 | 173 | .chrome-tab-drag-handle 174 | position absolute 175 | top 0 176 | bottom 0 177 | right 0 178 | left 0 179 | border-top-left-radius 8px 180 | border-top-right-radius 8px 181 | 182 | .chrome-tab-close 183 | flex-grow 0 184 | flex-shrink 0 185 | position relative 186 | width 16px 187 | height 16px 188 | border-radius 50% 189 | background-image url("data:image/svg+xml;utf8,") 190 | background-position center center 191 | background-repeat no-repeat 192 | background-size 8px 8px 193 | 194 | @media (hover: hover) 195 | &:hover 196 | background-color #e8eaed 197 | 198 | &:active 199 | background-color #dadce0 200 | 201 | @media not all and (hover: hover) 202 | &:active 203 | background-color #dadce0 204 | 205 | @media (hover: hover) 206 | &:not([active]) .chrome-tab-close:not(:hover):not(:active) 207 | opacity .8 208 | 209 | &[is-smaller] .chrome-tab-close 210 | margin-left auto 211 | 212 | &[is-mini]:not([active]) .chrome-tab-close 213 | display none 214 | 215 | &[is-mini][active] .chrome-tab-close 216 | margin-left auto 217 | margin-right auto 218 | 219 | &.chrome-tabs-is-sorting .chrome-tab:not(.chrome-tab-is-dragging) 220 | &:not(.chrome-tabs-is-sorting) .chrome-tab.chrome-tab-was-just-dragged 221 | transition transform 120ms ease-in-out 222 | 223 | .chrome-tabs-bottom-bar 224 | position absolute 225 | bottom 0 226 | height chromeTabSpaceBelow 227 | left 0 228 | width 100% 229 | background activeTabBackgroundColor 230 | z-index 10 231 | 232 | .chrome-tabs-optional-shadow-below-bottom-bar 233 | position relative 234 | height 1px 235 | width 100% 236 | background-image url("data:image/svg+xml;utf8,") 237 | background-size 1px 1px 238 | background-repeat repeat-x 239 | background-position 0% 0% 240 | 241 | @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) 242 | background-image url("data:image/svg+xml;utf8,") 243 | -------------------------------------------------------------------------------- /demo/css/demo.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 3 | background: #f1f3f4; 4 | } 5 | 6 | html, body { 7 | height: 100%; 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | html.dark-theme { 13 | background: #1c1c1c; 14 | } 15 | 16 | .surface { 17 | box-sizing: border-box; 18 | display: flex; 19 | width: 100%; 20 | min-height: 100%; 21 | padding-bottom: 3em; 22 | flex-direction: column; 23 | justify-content: center; 24 | } 25 | 26 | @media (min-width: 800px) { 27 | .surface { 28 | padding: 60px; 29 | } 30 | 31 | .mock-browser { 32 | width: 100%; 33 | background: #dee1e6; 34 | border-radius: 8px; 35 | flex-shrink: 0; 36 | overflow: hidden; 37 | } 38 | 39 | html.dark-theme .mock-browser { 40 | background: #202123; 41 | } 42 | 43 | .mock-browser-content { 44 | position: relative; 45 | z-index: 1; 46 | background: #fff; 47 | padding: 3em .5em; 48 | margin-top: -1px; 49 | } 50 | 51 | .chrome-tabs-optional-shadow-below-bottom-bar { 52 | z-index: 2; 53 | } 54 | 55 | html.dark-theme .mock-browser-content { 56 | background: #252729; 57 | } 58 | } 59 | 60 | 61 | @media (max-width: 799px) { 62 | html { 63 | background: #fff; 64 | } 65 | 66 | html.dark-theme { 67 | background: #252729; 68 | } 69 | 70 | .mock-browser-content { 71 | padding: 1em .5em; 72 | } 73 | 74 | .mock-browser .chrome-tabs { 75 | border-radius: 0; 76 | } 77 | } 78 | 79 | button { 80 | font-family: inherit; 81 | display: inline-block; 82 | cursor: pointer; 83 | background: #fff; 84 | font-size: 12px; 85 | border: 1px solid rgba(139, 141, 157, .4); 86 | border-radius: 3px; 87 | padding: .6em 1.5em .7em; 88 | border-radius: 9999px; 89 | } 90 | 91 | @supports (-webkit-overflow-scrolling: touch) { 92 | /* Prevent double tap to zoom on iOS https://stackoverflow.com/a/47131647 */ 93 | button { 94 | touch-action: manipulation; 95 | } 96 | } 97 | 98 | button:focus { 99 | border-color: #a9adb0; 100 | outline: none; 101 | box-shadow: 0 0 0 3px #dee1e6; 102 | } 103 | 104 | .buttons { 105 | margin-left: -.5em; 106 | text-align: center; 107 | } 108 | 109 | .buttons button { 110 | margin: .5em; 111 | } 112 | 113 | html.dark-theme button { 114 | -webkit-font-smoothing: antialiased; 115 | background: transparent; 116 | color: #fff; 117 | border-color: #4a4d51; 118 | } 119 | 120 | html.dark-theme button:focus { 121 | border-color: #858b90; 122 | box-shadow: 0 0 0 3px #4a4d51; 123 | } 124 | 125 | 126 | 127 | 128 | /* Carbon Ads */ 129 | 130 | .carbonads-wrapper { 131 | box-sizing: border-box; 132 | display: inline-block; 133 | margin-top: 60px; 134 | margin-bottom: 60px; 135 | } 136 | 137 | @media (max-width: 799px) { 138 | .carbonads-wrapper { 139 | margin-top: 20px; 140 | display: block; 141 | } 142 | } 143 | 144 | .carbonads-wrapper * { 145 | box-sizing: inherit; 146 | } 147 | 148 | #carbonads { 149 | -webkit-font-smoothing: antialiased; 150 | width: 162px; 151 | display: block; 152 | overflow: hidden; 153 | line-height: 1.33; 154 | width: 130px; 155 | max-width: 100%; 156 | font-size: 12px; 157 | margin: 0 auto; 158 | } 159 | 160 | #carbonads span { 161 | position: relative; 162 | display: block; 163 | overflow: hidden; 164 | } 165 | 166 | .carbon-img, .carbon-img img { 167 | display: block; 168 | width: 130px; 169 | height: 100px; 170 | } 171 | 172 | .carbon-img { 173 | margin-bottom: 7px; 174 | background: #ccc; 175 | } 176 | 177 | .carbon-img img { 178 | background: #fff; 179 | } 180 | 181 | .carbon-text { 182 | color: inherit; 183 | display: block; 184 | float: left; 185 | text-align: left; 186 | text-decoration: none; 187 | color: #666; 188 | line-height: 1.35; 189 | } 190 | 191 | .carbon-poweredby { 192 | color: #b9b9b9; 193 | text-decoration: none; 194 | display: block; 195 | text-align: left; 196 | font-size: 10px; 197 | margin-top: 5px; 198 | margin-bottom: 11px; 199 | } 200 | -------------------------------------------------------------------------------- /demo/images/facebook-favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamschwartz/chrome-tabs/1046a3fd4d164bc3550d82dc46ff1f13c6438e2f/demo/images/facebook-favicon.ico -------------------------------------------------------------------------------- /demo/images/google-favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamschwartz/chrome-tabs/1046a3fd4d164bc3550d82dc46ff1f13c6438e2f/demo/images/google-favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | Chrome Tabs - Chrome-style Tabs in HTML/CSS/ES6 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 |
Google
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 |
46 |
47 |
48 |
Facebook
49 |
50 |
51 |
52 |
53 |
54 |
55 | 56 | 69 |
70 |
71 |
72 |
73 | 74 | 75 | 76 | 77 |
78 |
79 |
80 | 81 |
82 | 83 |
84 | 85 | 107 | 108 |
109 | 110 | 111 |
Made by Adam Schwartz
112 |
113 |
114 |
115 | 116 | 117 | 118 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /js/chrome-tabs.js: -------------------------------------------------------------------------------- 1 | ((window, factory) => { 2 | if (typeof define == 'function' && define.amd) { 3 | define(['draggabilly'], Draggabilly => factory(window, Draggabilly)) 4 | } else if (typeof module == 'object' && module.exports) { 5 | module.exports = factory(window, require('draggabilly')) 6 | } else { 7 | window.ChromeTabs = factory(window, window.Draggabilly) 8 | } 9 | })(window, (window, Draggabilly) => { 10 | const TAB_CONTENT_MARGIN = 9 11 | const TAB_CONTENT_OVERLAP_DISTANCE = 1 12 | 13 | const TAB_OVERLAP_DISTANCE = (TAB_CONTENT_MARGIN * 2) + TAB_CONTENT_OVERLAP_DISTANCE 14 | 15 | const TAB_CONTENT_MIN_WIDTH = 24 16 | const TAB_CONTENT_MAX_WIDTH = 240 17 | 18 | const TAB_SIZE_SMALL = 84 19 | const TAB_SIZE_SMALLER = 60 20 | const TAB_SIZE_MINI = 48 21 | 22 | const noop = _ => {} 23 | 24 | const closest = (value, array) => { 25 | let closest = Infinity 26 | let closestIndex = -1 27 | 28 | array.forEach((v, i) => { 29 | if (Math.abs(value - v) < closest) { 30 | closest = Math.abs(value - v) 31 | closestIndex = i 32 | } 33 | }) 34 | 35 | return closestIndex 36 | } 37 | 38 | const tabTemplate = ` 39 |
40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | ` 52 | 53 | const defaultTapProperties = { 54 | title: 'New tab', 55 | favicon: false 56 | } 57 | 58 | let instanceId = 0 59 | 60 | class ChromeTabs { 61 | constructor() { 62 | this.draggabillies = [] 63 | } 64 | 65 | init(el) { 66 | this.el = el 67 | 68 | this.instanceId = instanceId 69 | this.el.setAttribute('data-chrome-tabs-instance-id', this.instanceId) 70 | instanceId += 1 71 | 72 | this.setupCustomProperties() 73 | this.setupStyleEl() 74 | this.setupEvents() 75 | this.layoutTabs() 76 | this.setupDraggabilly() 77 | } 78 | 79 | emit(eventName, data) { 80 | this.el.dispatchEvent(new CustomEvent(eventName, { detail: data })) 81 | } 82 | 83 | setupCustomProperties() { 84 | this.el.style.setProperty('--tab-content-margin', `${ TAB_CONTENT_MARGIN }px`) 85 | } 86 | 87 | setupStyleEl() { 88 | this.styleEl = document.createElement('style') 89 | this.el.appendChild(this.styleEl) 90 | } 91 | 92 | setupEvents() { 93 | window.addEventListener('resize', _ => { 94 | this.cleanUpPreviouslyDraggedTabs() 95 | this.layoutTabs() 96 | }) 97 | 98 | this.el.addEventListener('dblclick', event => { 99 | if ([this.el, this.tabContentEl].includes(event.target)) this.addTab() 100 | }) 101 | 102 | this.tabEls.forEach((tabEl) => this.setTabCloseEventListener(tabEl)) 103 | } 104 | 105 | get tabEls() { 106 | return Array.prototype.slice.call(this.el.querySelectorAll('.chrome-tab')) 107 | } 108 | 109 | get tabContentEl() { 110 | return this.el.querySelector('.chrome-tabs-content') 111 | } 112 | 113 | get tabContentWidths() { 114 | const numberOfTabs = this.tabEls.length 115 | const tabsContentWidth = this.tabContentEl.clientWidth 116 | const tabsCumulativeOverlappedWidth = (numberOfTabs - 1) * TAB_CONTENT_OVERLAP_DISTANCE 117 | const targetWidth = (tabsContentWidth - (2 * TAB_CONTENT_MARGIN) + tabsCumulativeOverlappedWidth) / numberOfTabs 118 | const clampedTargetWidth = Math.max(TAB_CONTENT_MIN_WIDTH, Math.min(TAB_CONTENT_MAX_WIDTH, targetWidth)) 119 | const flooredClampedTargetWidth = Math.floor(clampedTargetWidth) 120 | const totalTabsWidthUsingTarget = (flooredClampedTargetWidth * numberOfTabs) + (2 * TAB_CONTENT_MARGIN) - tabsCumulativeOverlappedWidth 121 | const totalExtraWidthDueToFlooring = tabsContentWidth - totalTabsWidthUsingTarget 122 | 123 | // TODO - Support tabs with different widths / e.g. "pinned" tabs 124 | const widths = [] 125 | let extraWidthRemaining = totalExtraWidthDueToFlooring 126 | for (let i = 0; i < numberOfTabs; i += 1) { 127 | const extraWidth = flooredClampedTargetWidth < TAB_CONTENT_MAX_WIDTH && extraWidthRemaining > 0 ? 1 : 0 128 | widths.push(flooredClampedTargetWidth + extraWidth) 129 | if (extraWidthRemaining > 0) extraWidthRemaining -= 1 130 | } 131 | 132 | return widths 133 | } 134 | 135 | get tabContentPositions() { 136 | const positions = [] 137 | const tabContentWidths = this.tabContentWidths 138 | 139 | let position = TAB_CONTENT_MARGIN 140 | tabContentWidths.forEach((width, i) => { 141 | const offset = i * TAB_CONTENT_OVERLAP_DISTANCE 142 | positions.push(position - offset) 143 | position += width 144 | }) 145 | 146 | return positions 147 | } 148 | 149 | get tabPositions() { 150 | const positions = [] 151 | 152 | this.tabContentPositions.forEach((contentPosition) => { 153 | positions.push(contentPosition - TAB_CONTENT_MARGIN) 154 | }) 155 | 156 | return positions 157 | } 158 | 159 | layoutTabs() { 160 | const tabContentWidths = this.tabContentWidths 161 | 162 | this.tabEls.forEach((tabEl, i) => { 163 | const contentWidth = tabContentWidths[i] 164 | const width = contentWidth + (2 * TAB_CONTENT_MARGIN) 165 | 166 | tabEl.style.width = width + 'px' 167 | tabEl.removeAttribute('is-small') 168 | tabEl.removeAttribute('is-smaller') 169 | tabEl.removeAttribute('is-mini') 170 | 171 | if (contentWidth < TAB_SIZE_SMALL) tabEl.setAttribute('is-small', '') 172 | if (contentWidth < TAB_SIZE_SMALLER) tabEl.setAttribute('is-smaller', '') 173 | if (contentWidth < TAB_SIZE_MINI) tabEl.setAttribute('is-mini', '') 174 | }) 175 | 176 | let styleHTML = '' 177 | this.tabPositions.forEach((position, i) => { 178 | styleHTML += ` 179 | .chrome-tabs[data-chrome-tabs-instance-id="${ this.instanceId }"] .chrome-tab:nth-child(${ i + 1 }) { 180 | transform: translate3d(${ position }px, 0, 0) 181 | } 182 | ` 183 | }) 184 | this.styleEl.innerHTML = styleHTML 185 | } 186 | 187 | createNewTabEl() { 188 | const div = document.createElement('div') 189 | div.innerHTML = tabTemplate 190 | return div.firstElementChild 191 | } 192 | 193 | addTab(tabProperties, { animate = true, background = false } = {}) { 194 | const tabEl = this.createNewTabEl() 195 | 196 | if (animate) { 197 | tabEl.classList.add('chrome-tab-was-just-added') 198 | setTimeout(() => tabEl.classList.remove('chrome-tab-was-just-added'), 500) 199 | } 200 | 201 | tabProperties = Object.assign({}, defaultTapProperties, tabProperties) 202 | this.tabContentEl.appendChild(tabEl) 203 | this.setTabCloseEventListener(tabEl) 204 | this.updateTab(tabEl, tabProperties) 205 | this.emit('tabAdd', { tabEl }) 206 | if (!background) this.setCurrentTab(tabEl) 207 | this.cleanUpPreviouslyDraggedTabs() 208 | this.layoutTabs() 209 | this.setupDraggabilly() 210 | } 211 | 212 | setTabCloseEventListener(tabEl) { 213 | tabEl.querySelector('.chrome-tab-close').addEventListener('click', _ => this.removeTab(tabEl)) 214 | } 215 | 216 | get activeTabEl() { 217 | return this.el.querySelector('.chrome-tab[active]') 218 | } 219 | 220 | hasActiveTab() { 221 | return !!this.activeTabEl 222 | } 223 | 224 | setCurrentTab(tabEl) { 225 | const activeTabEl = this.activeTabEl 226 | if (activeTabEl === tabEl) return 227 | if (activeTabEl) activeTabEl.removeAttribute('active') 228 | tabEl.setAttribute('active', '') 229 | this.emit('activeTabChange', { tabEl }) 230 | } 231 | 232 | removeTab(tabEl) { 233 | if (tabEl === this.activeTabEl) { 234 | if (tabEl.nextElementSibling) { 235 | this.setCurrentTab(tabEl.nextElementSibling) 236 | } else if (tabEl.previousElementSibling) { 237 | this.setCurrentTab(tabEl.previousElementSibling) 238 | } 239 | } 240 | tabEl.parentNode.removeChild(tabEl) 241 | this.emit('tabRemove', { tabEl }) 242 | this.cleanUpPreviouslyDraggedTabs() 243 | this.layoutTabs() 244 | this.setupDraggabilly() 245 | } 246 | 247 | updateTab(tabEl, tabProperties) { 248 | tabEl.querySelector('.chrome-tab-title').textContent = tabProperties.title 249 | 250 | const faviconEl = tabEl.querySelector('.chrome-tab-favicon') 251 | if (tabProperties.favicon) { 252 | faviconEl.style.backgroundImage = `url('${ tabProperties.favicon }')` 253 | faviconEl.removeAttribute('hidden', '') 254 | } else { 255 | faviconEl.setAttribute('hidden', '') 256 | faviconEl.removeAttribute('style') 257 | } 258 | 259 | if (tabProperties.id) { 260 | tabEl.setAttribute('data-tab-id', tabProperties.id) 261 | } 262 | } 263 | 264 | cleanUpPreviouslyDraggedTabs() { 265 | this.tabEls.forEach((tabEl) => tabEl.classList.remove('chrome-tab-was-just-dragged')) 266 | } 267 | 268 | setupDraggabilly() { 269 | const tabEls = this.tabEls 270 | const tabPositions = this.tabPositions 271 | 272 | if (this.isDragging) { 273 | this.isDragging = false 274 | this.el.classList.remove('chrome-tabs-is-sorting') 275 | this.draggabillyDragging.element.classList.remove('chrome-tab-is-dragging') 276 | this.draggabillyDragging.element.style.transform = '' 277 | this.draggabillyDragging.dragEnd() 278 | this.draggabillyDragging.isDragging = false 279 | this.draggabillyDragging.positionDrag = noop // Prevent Draggabilly from updating tabEl.style.transform in later frames 280 | this.draggabillyDragging.destroy() 281 | this.draggabillyDragging = null 282 | } 283 | 284 | this.draggabillies.forEach(d => d.destroy()) 285 | 286 | tabEls.forEach((tabEl, originalIndex) => { 287 | const originalTabPositionX = tabPositions[originalIndex] 288 | const draggabilly = new Draggabilly(tabEl, { 289 | axis: 'x', 290 | handle: '.chrome-tab-drag-handle', 291 | containment: this.tabContentEl 292 | }) 293 | 294 | this.draggabillies.push(draggabilly) 295 | 296 | draggabilly.on('pointerDown', _ => { 297 | this.setCurrentTab(tabEl) 298 | }) 299 | 300 | draggabilly.on('dragStart', _ => { 301 | this.isDragging = true 302 | this.draggabillyDragging = draggabilly 303 | tabEl.classList.add('chrome-tab-is-dragging') 304 | this.el.classList.add('chrome-tabs-is-sorting') 305 | }) 306 | 307 | draggabilly.on('dragEnd', _ => { 308 | this.isDragging = false 309 | const finalTranslateX = parseFloat(tabEl.style.left, 10) 310 | tabEl.style.transform = `translate3d(0, 0, 0)` 311 | 312 | // Animate dragged tab back into its place 313 | requestAnimationFrame(_ => { 314 | tabEl.style.left = '0' 315 | tabEl.style.transform = `translate3d(${ finalTranslateX }px, 0, 0)` 316 | 317 | requestAnimationFrame(_ => { 318 | tabEl.classList.remove('chrome-tab-is-dragging') 319 | this.el.classList.remove('chrome-tabs-is-sorting') 320 | 321 | tabEl.classList.add('chrome-tab-was-just-dragged') 322 | 323 | requestAnimationFrame(_ => { 324 | tabEl.style.transform = '' 325 | 326 | this.layoutTabs() 327 | this.setupDraggabilly() 328 | }) 329 | }) 330 | }) 331 | }) 332 | 333 | draggabilly.on('dragMove', (event, pointer, moveVector) => { 334 | // Current index be computed within the event since it can change during the dragMove 335 | const tabEls = this.tabEls 336 | const currentIndex = tabEls.indexOf(tabEl) 337 | 338 | const currentTabPositionX = originalTabPositionX + moveVector.x 339 | const destinationIndexTarget = closest(currentTabPositionX, tabPositions) 340 | const destinationIndex = Math.max(0, Math.min(tabEls.length, destinationIndexTarget)) 341 | 342 | if (currentIndex !== destinationIndex) { 343 | this.animateTabMove(tabEl, currentIndex, destinationIndex) 344 | } 345 | }) 346 | }) 347 | } 348 | 349 | animateTabMove(tabEl, originIndex, destinationIndex) { 350 | if (destinationIndex < originIndex) { 351 | tabEl.parentNode.insertBefore(tabEl, this.tabEls[destinationIndex]) 352 | } else { 353 | tabEl.parentNode.insertBefore(tabEl, this.tabEls[destinationIndex + 1]) 354 | } 355 | this.emit('tabReorder', { tabEl, originIndex, destinationIndex }) 356 | this.layoutTabs() 357 | } 358 | } 359 | 360 | return ChromeTabs 361 | }) 362 | -------------------------------------------------------------------------------- /older-versions.md: -------------------------------------------------------------------------------- 1 | ### Older versions 2 | 3 | This project attempts to keep up to date with the latest design of Google Chrome. Here is a list of prior visual styles: 4 | 5 | #### Chrome 53–69 rounded-style "Material Design" ([4ee0906b93](https://github.com/adamschwartz/chrome-tabs/tree/4ee0906b93c139e699fd289b4ef9f0dd16addf09)) 6 | 7 | 8 | 9 |




10 | 11 | #### Pre-Chrome 52 “Material Design” ([0c8c8f1880](https://github.com/adamschwartz/chrome-tabs/tree/0c8c8f18802cf67091151bb812d9693bee55b085)) 12 | 13 | ![](https://github.com/adamschwartz/chrome-tabs/raw/0c8c8f18802cf67091151bb812d9693bee55b085/chrome-tabs.gif) 14 | 15 | #### Pre rMBP ([c1b0b0eb8c](https://github.com/adamschwartz/chrome-tabs/tree/c1b0b0eb8c9d2452ee23520802abd7edf71200a8)) 16 | 17 | ![](https://github.com/adamschwartz/chrome-tabs/raw/c1b0b0eb8c9d2452ee23520802abd7edf71200a8/chrome-tabs.gif) 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-tabs", 3 | "version": "5.4.0", 4 | "description": "Chrome-style tabs in HTML/CSS/JS", 5 | "main": "js/chrome-tabs.js", 6 | "files": [ 7 | "README.md", 8 | "js/chrome-tabs.js", 9 | "css/*.css" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/adamschwartz/chrome-tabs.git" 14 | }, 15 | "keywords": [ 16 | "chrome", 17 | "tabs", 18 | "html" 19 | ], 20 | "author": "Adam Schwartz", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/adamschwartz/chrome-tabs/issues" 24 | }, 25 | "homepage": "https://github.com/adamschwartz/chrome-tabs#readme", 26 | "dependencies": { 27 | "draggabilly": "2.2.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /svg/tab.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamschwartz/chrome-tabs/1046a3fd4d164bc3550d82dc46ff1f13c6438e2f/svg/tab.sketch -------------------------------------------------------------------------------- /svg/tab.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | --------------------------------------------------------------------------------