├── .gitignore
├── svg
├── tab.sketch
└── tab.svg
├── chrome-tabs.gif
├── chrome-tabs.mov
├── demo
├── images
│ ├── google-favicon.ico
│ └── facebook-favicon.ico
└── css
│ └── demo.css
├── README.md
├── bower.json
├── package.json
├── older-versions.md
├── LICENSE.txt
├── css
├── chrome-tabs-dark-theme.styl
├── chrome-tabs-dark-theme.css
├── chrome-tabs.styl
└── chrome-tabs.css
├── index.html
└── js
└── chrome-tabs.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | bower_components/
3 | node_modules/
4 |
--------------------------------------------------------------------------------
/svg/tab.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamschwartz/chrome-tabs/HEAD/svg/tab.sketch
--------------------------------------------------------------------------------
/chrome-tabs.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamschwartz/chrome-tabs/HEAD/chrome-tabs.gif
--------------------------------------------------------------------------------
/chrome-tabs.mov:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamschwartz/chrome-tabs/HEAD/chrome-tabs.mov
--------------------------------------------------------------------------------
/demo/images/google-favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamschwartz/chrome-tabs/HEAD/demo/images/google-favicon.ico
--------------------------------------------------------------------------------
/demo/images/facebook-favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamschwartz/chrome-tabs/HEAD/demo/images/facebook-favicon.ico
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | 
14 |
15 | #### Pre rMBP ([c1b0b0eb8c](https://github.com/adamschwartz/chrome-tabs/tree/c1b0b0eb8c9d2452ee23520802abd7edf71200a8))
16 |
17 | 
18 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/svg/tab.svg:
--------------------------------------------------------------------------------
1 |
5 |
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-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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
34 |
35 |
36 |
Google
37 |
38 |
39 |
40 |
41 |
42 |
43 |
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 |
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 |
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 |
--------------------------------------------------------------------------------