├── .gitignore ├── CREDITS.md ├── LICENSE ├── README.md ├── background.js ├── content-script.js ├── devtools ├── index.html └── index.js ├── icons ├── icon_128.png ├── icon_144.png ├── icon_16.png ├── icon_192.png ├── icon_48.png ├── icon_72.png └── icon_96.png └── manifest.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | ## Icons 2 | 3 | Icons generated at [AndroidAssetStudio](https://romannurik.github.io/AndroidAssetStudio/) by 4 | [romannurik](https://github.com/romannurik) under a 5 | [Creative Commons Attribution 3.0 Unported License](http://creativecommons.org/licenses/by/3.0/) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Kerry Liu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Z-Context 2 | 3 | Z-Context is a Chrome DevTools Extension that displays stacking contexts and z-index values in the elements panel. 4 | 5 | ## Why use it? 6 | 7 | Browsers support a hierarchy of stacking contexts, rather than a single global one. This means that 8 | z-index values are often used incorrectly, and arbitrarily high values get set. To learn more read 9 | [Mozilla's guide on z-indexes](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index) 10 | or the [w3c spec](https://www.w3.org/TR/CSS2/zindex.html). 11 | 12 | By Using Z-Context, you'll know: 13 | 14 | * If the current element creates a stacking context, and why 15 | * What its parent stacking context is 16 | * The z-index value 17 | 18 | ## Install 19 | Visit https://chrome.google.com/webstore/detail/jigamimbjojkdgnlldajknogfgncplbh and add the extension to Chrome. 20 | 21 | ## See it in action: 22 | 23 | ![z-context](https://cldup.com/UivlV8iJ5q.gif) 24 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | const passMessagesFromDevtoolsToTab = ( port ) => { 2 | 3 | const sendMessagesToActiveTab = ( message ) => { 4 | chrome.tabs.query( { 5 | currentWindow: true, 6 | active: true, 7 | }, function ( tabs ) { 8 | if ( tabs.length > 0 ) { 9 | chrome.tabs.sendMessage( tabs[ 0 ].id, message ); 10 | } 11 | } ); 12 | }; 13 | 14 | const sendMessagesToDevTools = ( message, sender ) => { 15 | port.postMessage( message ); 16 | }; 17 | port.onMessage.addListener( sendMessagesToActiveTab ); 18 | 19 | // When a tab is closed, we should remove related listeners 20 | port.onDisconnect.addListener( function() { 21 | chrome.runtime.onMessage.removeListener( sendMessagesToDevTools ); 22 | } ); 23 | // Pass content script messages back to devtools 24 | chrome.runtime.onMessage.addListener( sendMessagesToDevTools ); 25 | } 26 | 27 | chrome.runtime.onConnect.addListener( passMessagesFromDevtoolsToTab ); 28 | -------------------------------------------------------------------------------- /content-script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Given a node element, generates a hopefully human-identifiable CSS selector. 3 | * @param {HTMLElement} element 4 | * @returns {string} CSS Selector 5 | */ 6 | function generateSelector( element ) { 7 | let selector, tag = element.nodeName.toLowerCase(); 8 | if ( element.id ) { 9 | selector = '#' + element.getAttribute( 'id' ); 10 | } else if ( element.getAttribute( 'class' ) ) { 11 | selector = '.' + element.getAttribute( 'class' ).split( ' ' ).join( '.' ); 12 | } 13 | return selector ? tag + selector : tag; 14 | } 15 | 16 | /** 17 | * @typedef {Object} StackingContext 18 | * 19 | * @property {Element} node A DOM Element 20 | * @property {string} reason Reason for why a stacking context was created 21 | */ 22 | 23 | /** 24 | * Recursive function that finds the closest parent stacking context. 25 | * See also https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context 26 | * 27 | * @param {Element} node 28 | * @returns {StackingContext} The closest parent stacking context 29 | */ 30 | const getClosestStackingContext = function ( node ) { 31 | // the root element (HTML). 32 | if ( ! node || node.nodeName === 'HTML' ) { 33 | return { node: document.documentElement, reason: 'root' }; 34 | } 35 | 36 | // handle shadow root elements. 37 | if ( node.nodeName === '#document-fragment' ) { 38 | return getClosestStackingContext( node.host ); 39 | } 40 | 41 | const computedStyle = getComputedStyle( node ); 42 | 43 | // position: fixed or sticky. 44 | if ( computedStyle.position === 'fixed' || computedStyle.position === 'sticky' ) { 45 | return { node: node, reason: `position: ${ computedStyle.position }` }; 46 | } 47 | 48 | // container-type: size or inline-size 49 | if ( computedStyle.containerType === 'size' || computedStyle.containerType === 'inline-size' ) { 50 | return { node: node, reason: `container-type: ${computedStyle.containerType}`} 51 | } 52 | 53 | // positioned (absolutely or relatively) with a z-index value other than "auto". 54 | if ( computedStyle.zIndex !== 'auto' && computedStyle.position !== 'static' ) { 55 | return { node: node, reason: `position: ${ computedStyle.position }; z-index: ${ computedStyle.zIndex }` }; 56 | } 57 | 58 | // elements with an opacity value less than 1. 59 | if ( computedStyle.opacity !== '1' ) { 60 | return { node: node, reason: `opacity: ${ computedStyle.opacity }` }; 61 | } 62 | 63 | // elements with a transform value other than "none". 64 | if ( computedStyle.transform !== 'none' ) { 65 | return { node: node, reason: `transform: ${ computedStyle.transform }` }; 66 | } 67 | 68 | // elements with a scale value other than "none" 69 | if ( computedStyle.scale !== 'none' ) { 70 | return { node: node, reason: `scale: ${ computedStyle.scale }` }; 71 | } 72 | 73 | // elements with a rotate value other than "none" 74 | if ( computedStyle.rotate !== 'none' ) { 75 | return { node: node, reason: `rotate: ${ computedStyle.rotate }` }; 76 | } 77 | 78 | // elements with a translate value other than "none" 79 | if ( computedStyle.translate !== 'none' ) { 80 | return { node: node, reason: `translate: ${ computedStyle.translate }` }; 81 | } 82 | 83 | // elements with a mix-blend-mode value other than "normal". 84 | if ( computedStyle.mixBlendMode !== 'normal' ) { 85 | return { node: node, reason: `mixBlendMode: ${ computedStyle.mixBlendMode }` }; 86 | } 87 | 88 | // elements with a filter value other than "none". 89 | if ( computedStyle.filter !== 'none' ) { 90 | return { node: node, reason: `filter: ${ computedStyle.filter }` }; 91 | } 92 | 93 | // elements with a backdropFilter value other than "none". 94 | if ( computedStyle.backdropFilter !== 'none' ) { 95 | return { node: node, reason: `backdropFilter: ${ computedStyle.backdropFilter }` }; 96 | } 97 | 98 | // elements with a perspective value other than "none". 99 | if ( computedStyle.perspective !== 'none' ) { 100 | return { node: node, reason: `perspective: ${ computedStyle.perspective }` }; 101 | } 102 | 103 | // elements with a clip-path value other than "none". 104 | if ( computedStyle.clipPath !== 'none' ) { 105 | return { node: node, reason: `clip-path: ${ computedStyle.clipPath } ` }; 106 | } 107 | 108 | // elements with a mask value other than "none". 109 | const mask = computedStyle.mask || computedStyle.webkitMask; 110 | if ( mask !== 'none' && mask !== undefined ) { 111 | return { node: node, reason: `mask: ${ mask }` }; 112 | } 113 | 114 | // elements with a mask-image value other than "none". 115 | const maskImage = computedStyle.maskImage || computedStyle.webkitMaskImage; 116 | if ( maskImage !== 'none' && maskImage !== undefined ) { 117 | return { node: node, reason: `mask-image: ${ maskImage }` }; 118 | } 119 | 120 | // elements with a mask-border value other than "none". 121 | const maskBorder = computedStyle.maskBorder || computedStyle.webkitMaskBorder; 122 | if ( maskBorder !== 'none' && maskBorder !== undefined ) { 123 | return { node: node, reason: `mask-border: ${ maskBorder }` }; 124 | } 125 | 126 | // elements with isolation set to "isolate". 127 | if ( computedStyle.isolation === 'isolate' ) { 128 | return { node: node, reason: `isolation: ${ computedStyle.isolation }` }; 129 | } 130 | 131 | // transform or opacity in will-change even if you don't specify values for these attributes directly. 132 | if ( computedStyle.willChange === 'transform' || computedStyle.willChange === 'opacity' ) { 133 | return { node: node, reason: `willChange: ${ computedStyle.willChange }` }; 134 | } 135 | 136 | // elements with -webkit-overflow-scrolling set to "touch". 137 | if ( computedStyle.webkitOverflowScrolling === 'touch' ) { 138 | return { node: node, reason: '-webkit-overflow-scrolling: touch' }; 139 | } 140 | 141 | // an item with a z-index value other than "auto". 142 | if ( computedStyle.zIndex !== 'auto' ) { 143 | const parentStyle = getComputedStyle( node.parentNode ); 144 | // with a flex|inline-flex parent. 145 | if ( parentStyle.display === 'flex' || parentStyle.display === 'inline-flex' ) { 146 | return { 147 | node: node, 148 | reason: `flex-item; z-index: ${ computedStyle.zIndex }`, 149 | }; 150 | // with a grid parent. 151 | } else if ( parentStyle.grid !== 'none / none / none / row / auto / auto' ) { 152 | return { 153 | node: node, 154 | reason: `child of grid container; z-index: ${ computedStyle.zIndex }`, 155 | }; 156 | } 157 | } 158 | 159 | // contain with a value of layout, or paint, or a composite value that includes either of them 160 | const contain = computedStyle.contain; 161 | if ( [ 'layout', 'paint', 'strict', 'content' ].indexOf( contain ) > -1 || 162 | contain.indexOf( 'paint' ) > -1 || 163 | contain.indexOf( 'layout' ) > -1 164 | ) { 165 | return { 166 | node: node, 167 | reason: `contain: ${ contain }`, 168 | }; 169 | } 170 | 171 | return getClosestStackingContext( node.parentNode ); 172 | }; 173 | 174 | /** 175 | * @typedef {Object} ZContextSidebarContents 176 | * 177 | * @property {boolean} [createsStackingContext] True if element creates a stacking context. 178 | * @property {string} [createsStackingContextReason] Reason for why a stacking context is created. 179 | * @property {string} [parentStackingContext] Human readable CSS selector of the parent stacking context. 180 | * @property {number} [z-index] The current z-index value 181 | */ 182 | 183 | /** 184 | * @typedef {Object} ZContextUpdateSidebarAction 185 | * 186 | * @property {string} type The action name 187 | * @property {ZContextSidebarContents} sidebar Contents to update the z-index pane with 188 | */ 189 | 190 | /** 191 | * Given an element, looks up the related z-index and stacking context information and returns a ZContextUpdateSidebarAction 192 | * 193 | * @param {Element} element 194 | * @returns {ZContextUpdateSidebarAction} 195 | */ 196 | function zContext( element ) { 197 | if ( ! element || element.nodeType !== 1 ) { 198 | return { type: 'Z_CONTEXT_UPDATE_SIDEBAR', sidebar: {} }; 199 | } 200 | if ( element && element.nodeType === 1 ) { 201 | const closest = getClosestStackingContext( element ); 202 | const createsStackingContext = element === closest.node; 203 | const reason = createsStackingContext ? closest.reason : 'not a stacking context'; 204 | let parentContext = closest.node; 205 | const computedStyle = getComputedStyle( element ); 206 | if ( createsStackingContext && element.nodeName !== 'HTML' ) { 207 | parentContext = getClosestStackingContext( $0.parentNode ).node; 208 | } 209 | return { 210 | type: 'Z_CONTEXT_UPDATE_SIDEBAR', 211 | sidebar: { 212 | createsStackingContext, 213 | createsStackingContextReason: reason, 214 | parentStackingContext: generateSelector( parentContext ), 215 | currentNode: generateSelector( element ), 216 | 'z-index': computedStyle.zIndex !== 'auto' ? parseInt( computedStyle.zIndex, 10 ) : computedStyle.zIndex 217 | }, 218 | }; 219 | } 220 | } 221 | 222 | /** 223 | * Stores the last selected element $0 in this frame. 224 | */ 225 | let _lastElement; 226 | 227 | /** 228 | * Invoked by the devtools panel when a new element is selected. This function sends a new message to update the 229 | * z-index pane with the z-index stacking context information if we detect that an element has been selected 230 | * in this frame. 231 | * 232 | * @param {node} element 233 | * @returns void 234 | */ 235 | function setSelectedElement( element ) { 236 | // If the selected element is the same, let handlers in other iframe contexts handle it instead. 237 | if ( element !== undefined && element !== _lastElement ) { 238 | _lastElement = element; 239 | chrome.runtime.sendMessage( zContext( element ) ); 240 | } 241 | } 242 | 243 | /** 244 | * Listen for the z-index devtools panel to be created, before registering the frame. 245 | */ 246 | chrome.runtime.onMessage.addListener( function ( message ) { 247 | if ( message.type === 'Z_CONTEXT_SIDEBAR_INIT' ) { 248 | chrome.runtime.sendMessage( { type: 'Z_CONTEXT_REGISTER_FRAME', url: window.location.href } ); 249 | } 250 | } ); 251 | 252 | /** 253 | * Reconnect to the z-index devtools panel when we navigate to another page 254 | */ 255 | function addLocationObserver(callback) { 256 | const config = { attributes: false, childList: true, subtree: false } 257 | const observer = new MutationObserver(callback) 258 | observer.observe(document.body, config) 259 | } 260 | const reregisterOnNavigation = () => { 261 | chrome.runtime.sendMessage( { type: 'Z_CONTEXT_NAVIGATION' } ); 262 | } 263 | addLocationObserver(reregisterOnNavigation) 264 | -------------------------------------------------------------------------------- /devtools/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /devtools/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates the z-index sidebar pane 3 | */ 4 | chrome.devtools.panels.elements.createSidebarPane( 5 | "Z-Index", 6 | function ( sidebar ) { 7 | const port = chrome.runtime.connect( { name: "Z-Context" } ); 8 | // Listen for messages sent from background.js. 9 | port.onMessage.addListener( function ( msg ) { 10 | switch ( msg.type ) { 11 | case 'Z_CONTEXT_REGISTER_FRAME': { 12 | // Each frame should listen to onSelectionChanged events. 13 | chrome.devtools.panels.elements.onSelectionChanged.addListener( 14 | () => { 15 | chrome.devtools.inspectedWindow.eval( "setSelectedElement($0)", { 16 | useContentScriptContext: true, 17 | frameURL: msg.url 18 | } ); 19 | } 20 | ); 21 | // Populate initial opening. 22 | chrome.devtools.inspectedWindow.eval( "setSelectedElement($0)", { 23 | useContentScriptContext: true, 24 | frameURL: msg.url 25 | } ); 26 | break; 27 | } 28 | case 'Z_CONTEXT_UPDATE_SIDEBAR': { 29 | sidebar.setObject( msg.sidebar ); 30 | break; 31 | } 32 | case 'Z_CONTEXT_NAVIGATION': { 33 | port.postMessage( { type: 'Z_CONTEXT_SIDEBAR_INIT' } ); 34 | break; 35 | } 36 | } 37 | } ); 38 | // Announce to content-scripts.js that they should register with their frame urls. 39 | port.postMessage( { type: 'Z_CONTEXT_SIDEBAR_INIT' } ); 40 | } 41 | ); 42 | -------------------------------------------------------------------------------- /icons/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwwar/z-context/dea7c1c220c77281ce6a02b910460b3a5d4744c8/icons/icon_128.png -------------------------------------------------------------------------------- /icons/icon_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwwar/z-context/dea7c1c220c77281ce6a02b910460b3a5d4744c8/icons/icon_144.png -------------------------------------------------------------------------------- /icons/icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwwar/z-context/dea7c1c220c77281ce6a02b910460b3a5d4744c8/icons/icon_16.png -------------------------------------------------------------------------------- /icons/icon_192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwwar/z-context/dea7c1c220c77281ce6a02b910460b3a5d4744c8/icons/icon_192.png -------------------------------------------------------------------------------- /icons/icon_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwwar/z-context/dea7c1c220c77281ce6a02b910460b3a5d4744c8/icons/icon_48.png -------------------------------------------------------------------------------- /icons/icon_72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwwar/z-context/dea7c1c220c77281ce6a02b910460b3a5d4744c8/icons/icon_72.png -------------------------------------------------------------------------------- /icons/icon_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwwar/z-context/dea7c1c220c77281ce6a02b910460b3a5d4744c8/icons/icon_96.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "z-context", 4 | "version": "3.0.0", 5 | "description": "A Chrome DevTools Extension that displays stacking contexts and z-index values in the elements panel", 6 | "author": "gwwar", 7 | "devtools_page": "devtools/index.html", 8 | "background": { 9 | "service_worker": "background.js" 10 | }, 11 | "content_scripts": [ 12 | { 13 | "matches": [ 14 | "http://*/*", 15 | "https://*/*" 16 | ], 17 | "js": [ 18 | "content-script.js" 19 | ], 20 | "all_frames": true, 21 | "run_at": "document_end" 22 | } 23 | ], 24 | "icons": { 25 | "16": "icons/icon_16.png", 26 | "48": "icons/icon_48.png", 27 | "72": "icons/icon_72.png", 28 | "96": "icons/icon_96.png", 29 | "128": "icons/icon_128.png", 30 | "144": "icons/icon_144.png", 31 | "192": "icons/icon_192.png" 32 | } 33 | } --------------------------------------------------------------------------------