├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── test ├── samples │ ├── nested-z-index.html │ ├── ordered.html │ ├── shadow-dom.html │ ├── static-shadow-dom.html │ ├── static-z-index.html │ └── z-index.html ├── templates │ └── page.html └── test.js ├── tsconfig.json └── types └── index.d.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | # cancel in-progress runs on new commits to same PR (gitub.event.number) 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.event.number || github.sha }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | Tests: 16 | runs-on: ${{ matrix.os }} 17 | timeout-minutes: 30 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | node-version: [16] 22 | os: [ubuntu-latest] 23 | steps: 24 | - run: git config --global core.autocrlf false 25 | - uses: actions/checkout@v3 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - run: npm ci 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "useTabs": true 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # stacking-order changelog 2 | 3 | ## 2.0.0 4 | 5 | - Convert to ESM 6 | 7 | ## 1.0.1 8 | 9 | - Support shadow DOM ([#1](https://gitlab.com/Rich-Harris/stacking-order/-/merge_requests/1)) 10 | 11 | ## 1.0.0 12 | 13 | - First release 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # contributing to stacking-order 2 | 3 | If you find a bug, please create a pull request containing a minimal reproduction in the [test/samples](https://gitlab.com/Rich-Harris/stacking-order/tree/master/test/samples) directory. Your test is an HTML snippet containing one element with a `data-front` attribute and one with a `data-back` attribute – obviously `data-front` should render in front of `data-back`. 4 | 5 | If you *fix* a bug, even better! Thanks in advance. 6 | 7 | ## Running the tests 8 | 9 | After cloning the repo, install all the development dependencies with `npm install` and run the tests with `npm test`. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stacking-order 2 | 3 | Determine which of two nodes appears in front of the other. 4 | 5 | ## Why? 6 | 7 | The [stacking order rules](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context) are fairly complex. Determining whether node A will render in front of node B involves much more than comparing the `z-index` of the two nodes – you have to consider their parents, and which of them create new _stacking contexts_, which in turn depends on CSS properties like `opacity`, `transform`, `mix-blend-mode` and various others that you probably hadn't considered. 8 | 9 | The tie-breaker, if that doesn't yield a conclusive answer, is the position in the document (with later nodes rendering in front of earlier nodes). 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm install --save stacking-order 15 | ``` 16 | 17 | ...or grab a copy from [npmcdn.com/stacking-order](https://npmcdn.com/stacking-order). 18 | 19 | ## Usage 20 | 21 | ```js 22 | import { compare } from 'stacking-order'; 23 | 24 | const a = document.querySelector('.a'); 25 | const b = document.querySelector('.b'); 26 | 27 | const order = compare(a, b); 28 | // -> `1` if a is in front of b, `-1` otherwise 29 | ``` 30 | 31 | ## Bugs 32 | 33 | It's entirely possible that the algorithm used here doesn't exactly match the spec. If you find a bug, please [raise an issue](https://github.com/Rich-Harris/stacking-order/issues) after reading [CONTRIBUTING.md](CONTRIBUTING.md). Thanks! 34 | 35 | ## License 36 | 37 | MIT 38 | 39 | --- 40 | 41 | made by [@rich_harris](https://twitter.com/rich_harris) 42 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Determine which of two nodes appears in front of the other — 3 | * if `a` is in front, returns 1, otherwise returns -1 4 | * @param {HTMLElement} a 5 | * @param {HTMLElement} b 6 | */ 7 | export function compare(a, b) { 8 | if (a === b) throw new Error('Cannot compare node with itself'); 9 | 10 | const ancestors = { 11 | a: get_ancestors(a), 12 | b: get_ancestors(b), 13 | }; 14 | 15 | let common_ancestor; 16 | 17 | // remove shared ancestors 18 | while (ancestors.a.at(-1) === ancestors.b.at(-1)) { 19 | a = ancestors.a.pop(); 20 | b = ancestors.b.pop(); 21 | 22 | common_ancestor = a; 23 | } 24 | 25 | const z_indexes = { 26 | a: get_z_index(find_stacking_context(ancestors.a)), 27 | b: get_z_index(find_stacking_context(ancestors.b)), 28 | }; 29 | 30 | if (z_indexes.a === z_indexes.b) { 31 | const children = common_ancestor.childNodes; 32 | 33 | const furthest_ancestors = { 34 | a: ancestors.a.at(-1), 35 | b: ancestors.b.at(-1), 36 | }; 37 | 38 | let i = children.length; 39 | while (i--) { 40 | const child = children[i]; 41 | if (child === furthest_ancestors.a) return 1; 42 | if (child === furthest_ancestors.b) return -1; 43 | } 44 | } 45 | 46 | return Math.sign(z_indexes.a - z_indexes.b); 47 | } 48 | 49 | const props = /\b(?:position|zIndex|opacity|transform|webkitTransform|mixBlendMode|filter|webkitFilter|isolation)\b/; 50 | 51 | /** @param {HTMLElement} node */ 52 | function is_flex_item(node) { 53 | const display = getComputedStyle(get_parent(node)).display; 54 | return display === 'flex' || display === 'inline-flex'; 55 | } 56 | 57 | /** @param {HTMLElement} node */ 58 | function creates_stacking_context(node) { 59 | const style = getComputedStyle(node); 60 | 61 | // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context 62 | if (style.position === 'fixed') return true; 63 | if ((style.zIndex !== 'auto' && style.position !== 'static') || is_flex_item(node)) return true; 64 | if (+style.opacity < 1) return true; 65 | if ('transform' in style && style.transform !== 'none') return true; 66 | if ('webkitTransform' in style && style.webkitTransform !== 'none') return true; 67 | if ('mixBlendMode' in style && style.mixBlendMode !== 'normal') return true; 68 | if ('filter' in style && style.filter !== 'none') return true; 69 | if ('webkitFilter' in style && style.webkitFilter !== 'none') return true; 70 | if ('isolation' in style && style.isolation === 'isolate') return true; 71 | if (props.test(style.willChange)) return true; 72 | // @ts-expect-error 73 | if (style.webkitOverflowScrolling === 'touch') return true; 74 | 75 | return false; 76 | } 77 | 78 | /** @param {HTMLElement[]} nodes */ 79 | function find_stacking_context(nodes) { 80 | let i = nodes.length; 81 | 82 | while (i--) { 83 | if (creates_stacking_context(nodes[i])) return nodes[i]; 84 | } 85 | 86 | return null; 87 | } 88 | 89 | /** @param {HTMLElement} node */ 90 | function get_z_index(node) { 91 | return (node && Number(getComputedStyle(node).zIndex)) || 0; 92 | } 93 | 94 | /** @param {HTMLElement} node */ 95 | function get_ancestors(node) { 96 | const ancestors = []; 97 | 98 | while (node) { 99 | ancestors.push(node); 100 | node = get_parent(node); 101 | } 102 | 103 | return ancestors; // [ node, ...
, , document ] 104 | } 105 | 106 | /** @param {HTMLElement} node */ 107 | function get_parent(node) { 108 | // @ts-ignore 109 | return node.parentNode?.host || node.parentNode; 110 | } 111 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stacking-order", 3 | "version": "2.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "stacking-order", 9 | "version": "2.0.0", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "kleur": "^4.1.5", 13 | "playwright": "^1.28.0", 14 | "typescript": "^4.9.3" 15 | } 16 | }, 17 | "node_modules/kleur": { 18 | "version": "4.1.5", 19 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", 20 | "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", 21 | "dev": true, 22 | "engines": { 23 | "node": ">=6" 24 | } 25 | }, 26 | "node_modules/playwright": { 27 | "version": "1.28.0", 28 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.28.0.tgz", 29 | "integrity": "sha512-kyOXGc5y1mgi+hgEcCIyE1P1+JumLrxS09nFHo5sdJNzrucxPRAGwM4A2X3u3SDOfdgJqx61yIoR6Av+5plJPg==", 30 | "dev": true, 31 | "hasInstallScript": true, 32 | "dependencies": { 33 | "playwright-core": "1.28.0" 34 | }, 35 | "bin": { 36 | "playwright": "cli.js" 37 | }, 38 | "engines": { 39 | "node": ">=14" 40 | } 41 | }, 42 | "node_modules/playwright-core": { 43 | "version": "1.28.0", 44 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz", 45 | "integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==", 46 | "dev": true, 47 | "bin": { 48 | "playwright": "cli.js" 49 | }, 50 | "engines": { 51 | "node": ">=14" 52 | } 53 | }, 54 | "node_modules/typescript": { 55 | "version": "4.9.3", 56 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", 57 | "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", 58 | "dev": true, 59 | "bin": { 60 | "tsc": "bin/tsc", 61 | "tsserver": "bin/tsserver" 62 | }, 63 | "engines": { 64 | "node": ">=4.2.0" 65 | } 66 | } 67 | }, 68 | "dependencies": { 69 | "kleur": { 70 | "version": "4.1.5", 71 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", 72 | "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", 73 | "dev": true 74 | }, 75 | "playwright": { 76 | "version": "1.28.0", 77 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.28.0.tgz", 78 | "integrity": "sha512-kyOXGc5y1mgi+hgEcCIyE1P1+JumLrxS09nFHo5sdJNzrucxPRAGwM4A2X3u3SDOfdgJqx61yIoR6Av+5plJPg==", 79 | "dev": true, 80 | "requires": { 81 | "playwright-core": "1.28.0" 82 | } 83 | }, 84 | "playwright-core": { 85 | "version": "1.28.0", 86 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz", 87 | "integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==", 88 | "dev": true 89 | }, 90 | "typescript": { 91 | "version": "4.9.3", 92 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", 93 | "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", 94 | "dev": true 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stacking-order", 3 | "description": "Determine which of two elements is in front of the other", 4 | "version": "2.0.0", 5 | "author": "Rich Harris", 6 | "type": "module", 7 | "main": "index.js", 8 | "repository": "https://github.com/Rich-Harris/stacking-order.git", 9 | "license": "MIT", 10 | "devDependencies": { 11 | "kleur": "^4.1.5", 12 | "playwright": "^1.28.0", 13 | "typescript": "^4.9.3" 14 | }, 15 | "files": [ 16 | "index.js", 17 | "README.md" 18 | ], 19 | "exports": { 20 | ".": { 21 | "types": "./types/index.d.ts", 22 | "import": "./index.js", 23 | "default": "./index.js" 24 | } 25 | }, 26 | "scripts": { 27 | "prepublishOnly": "npm test && tsc", 28 | "test": "node test/test.js" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/samples/nested-z-index.html: -------------------------------------------------------------------------------- 1 |