├── .nojekyll ├── .gitignore ├── site ├── assets │ ├── logo.png │ ├── favicon.png │ ├── chrome.svg │ ├── edge.svg │ ├── safari.svg │ ├── firefox.svg │ ├── timeline-durations.json │ ├── search.js │ └── styles.css ├── feature-json.njk ├── _includes │ ├── wpt.njk │ ├── position_per_browser.njk │ ├── interop.njk │ ├── surveys.njk │ ├── use_counters.njk │ ├── baseline.njk │ ├── position.njk │ ├── tags.njk │ ├── compat_short.njk │ ├── specs.njk │ ├── feature_short.njk │ ├── origin_trials.njk │ ├── discouraged.njk │ ├── feature_list_views.njk │ ├── docs.njk │ ├── compat_full.njk │ ├── layout.njk │ └── feature_full.njk ├── feature.njk ├── features.njk ├── all.njk ├── discouraged.njk ├── release-notes.njk ├── widely-available.njk ├── one-missing-engine.njk ├── one-missing-engine-oldest.njk ├── browser.njk ├── ids.njk ├── limited-availability.njk ├── newly-available.njk ├── index.njk ├── unordinary-feature.njk ├── bcd-mapping.njk ├── browse.njk ├── groups.njk ├── newly-available-feed.njk ├── widely-available-feed.njk ├── about.njk ├── filter.njk ├── monthly-updates.njk ├── monthly-updates-feed.njk ├── timeline.njk └── _data │ └── featureCatalog.yml ├── additional-data ├── README.md ├── scripts │ ├── update-interop.js │ ├── update-wpt.js │ ├── check-docs.js │ ├── update-origin-trials.js │ ├── update-state-of.js │ ├── update-use-counters.js │ └── update-standard-positions.js ├── interop.json └── wpt.json ├── .vscode └── launch.json ├── package.json ├── check-groups.js ├── .github └── workflows │ ├── update-wpt.yml │ ├── update-interop.yml │ ├── update-origin-trials.yml │ ├── update-standard-positions.yml │ ├── update-use-counters.yml │ ├── update-state-of-surveys.yml │ └── generate-site.yml ├── README.md ├── update-timeline-stats.js ├── LICENSE.txt └── .eleventy.js /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | docs 3 | -------------------------------------------------------------------------------- /site/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elchi3/web-features-explorer/main/site/assets/logo.png -------------------------------------------------------------------------------- /site/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elchi3/web-features-explorer/main/site/assets/favicon.png -------------------------------------------------------------------------------- /site/feature-json.njk: -------------------------------------------------------------------------------- 1 | --- 2 | pagination: 3 | data: allFeatures 4 | size: 1 5 | alias: feature 6 | permalink: "features/{{ feature.id | slugify }}.json" 7 | --- 8 | {{ feature | stringify | safe }} -------------------------------------------------------------------------------- /additional-data/README.md: -------------------------------------------------------------------------------- 1 | # Additional data 2 | 3 | This folder contains additional data that's mapped to web-feature IDs. 4 | The `script` sub-folder contains scripts that can be used to re-generate the data. -------------------------------------------------------------------------------- /site/_includes/wpt.njk: -------------------------------------------------------------------------------- 1 | {% if feature.wpt %} 2 |
3 |

Web Platform Tests (WPT)

4 | View the latest WPT test results for this feature 5 |
6 | {% endif %} -------------------------------------------------------------------------------- /site/feature.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layout.njk 3 | pagination: 4 | data: allFeatures 5 | size: 1 6 | alias: feature 7 | permalink: "features/{{ feature.id | slugify }}/" 8 | eleventyComputed: 9 | title: "{% prettyFeatureName feature.name %}" 10 | --- 11 | 12 |
13 | {% include "feature_full.njk" %} 14 |
-------------------------------------------------------------------------------- /site/_includes/position_per_browser.njk: -------------------------------------------------------------------------------- 1 | {% if browser.id === "safari" or browser.id === "safari_ios" %} 2 | {% set position = feature.standardPositions.webkit %} 3 | {% include "position.njk" %} 4 | {% elif browser.id === "firefox" or browser.id === "firefox_android" %} 5 | {% set position = feature.standardPositions.mozilla %} 6 | {% include "position.njk" %} 7 | {% endif %} -------------------------------------------------------------------------------- /site/features.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: All features 3 | layout: layout.njk 4 | --- 5 | 6 |
7 | {% include "feature_list_views.njk" %} 8 | 9 |

{{ title }}

10 | 11 | 18 |
-------------------------------------------------------------------------------- /site/all.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: All features 3 | layout: layout.njk 4 | --- 5 | 6 |
7 | {% include "feature_list_views.njk" %} 8 | 9 |

{{ title }}

10 | 11 | 18 |
19 | -------------------------------------------------------------------------------- /site/_includes/interop.njk: -------------------------------------------------------------------------------- 1 | {% if feature.interop.length %} 2 |
3 |

Interop

4 | 11 |
12 | {% endif %} -------------------------------------------------------------------------------- /site/_includes/surveys.njk: -------------------------------------------------------------------------------- 1 | {% if feature.stateOfSurveys.length %} 2 |
3 |

Developer signals

4 | 11 |
12 | {% endif %} -------------------------------------------------------------------------------- /site/assets/chrome.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/_includes/use_counters.njk: -------------------------------------------------------------------------------- 1 | {% if feature.useCounters.percentageOfPageLoad %} 2 |
3 |

Usage (according to Chrome Platform Status)

4 |

{% useCounterPercentage feature.useCounters.percentageOfPageLoad %} of page loads. More data at chromestatus.

5 |
6 | {% endif %} -------------------------------------------------------------------------------- /site/_includes/baseline.njk: -------------------------------------------------------------------------------- 1 |
2 | {% if feature.discouraged %} 3 | Discouraged 4 | {% else %} 5 | {% if not feature.status.baseline %}Limited availability{% endif %} 6 | {% if feature.status.baseline === "low" %}Baseline Newly Available (since {% baselineDate feature.status.baseline_low_date %}){% endif %} 7 | {% if feature.status.baseline === "high" %}Baseline Widely Available (since {% baselineDate feature.status.baseline_high_date %}){% endif %} 8 | {% endif %} 9 |
-------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Build the site", 11 | "runtimeExecutable": "npm", 12 | "runtimeArgs": [ 13 | "run", 14 | "build" 15 | ], 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /site/_includes/position.njk: -------------------------------------------------------------------------------- 1 | {% if position and position.url %} 2 | 3 | Vendor position 4 | {% if position.position %} 5 | : {{ position.position }} 6 | {% else %} 7 | : unknown 8 | {% endif %} 9 | {% if position.concerns.length > 0 %} 10 | (concerns: {{ position.concerns | join(", ") }}) 11 | {% endif %} 12 | 13 | {% endif %} 14 | -------------------------------------------------------------------------------- /site/_includes/tags.njk: -------------------------------------------------------------------------------- 1 |
2 | {% if feature.bcdTags.length %} 3 | Tags: 4 | {% for tag in feature.bcdTags %} 5 | {{ tag }} 6 | {% endfor %} 7 | {% endif %} 8 | {% if feature.groupPaths.length %} 9 | Groups: 10 | {% for path in feature.groupPaths %} 11 | 12 | {% for part in path %} 13 | {{ part }} {% if not loop.last %} > {% endif %} 14 | {% endfor %} 15 | 16 | {% endfor %} 17 | {% endif %} 18 |
19 | -------------------------------------------------------------------------------- /site/_includes/compat_short.njk: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/discouraged.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Discouraged features 3 | layout: layout.njk 4 | --- 5 | 6 |
7 | {% include "feature_list_views.njk" %} 8 | 9 |

{{ title }}

10 | 11 |

Using the following features is discouraged, even if they're still implemented in browsers, because standards bodies and/or browser implementers have agreed that they should not be used.

12 | 13 | 22 |
23 | -------------------------------------------------------------------------------- /site/_includes/specs.njk: -------------------------------------------------------------------------------- 1 | {% if feature.spec.length %} 2 |
3 |

Specifications

4 | 16 |
17 | {% endif %} -------------------------------------------------------------------------------- /site/release-notes.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: All release notes 3 | layout: layout.njk 4 | --- 5 | 6 |
7 |

{{ title }}

8 | 9 | 25 |
26 | -------------------------------------------------------------------------------- /site/_includes/feature_short.njk: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{% prettyFeatureName feature.name %}

4 | {% include "baseline.njk" %} 5 |
6 | 7 |

{{ feature.description_html | safe }}

8 | 9 | {%- if feature.blockedOn %} 10 |

Baseline availability blocked since {{ feature.blockedSince }} by {{ feature.blockedOn }} ({{ feature.monthsBlocked }} months)

11 | {%- endif %} 12 | 13 |

Learn more

14 | 15 | {% include "compat_short.njk" %} 16 |
-------------------------------------------------------------------------------- /site/_includes/origin_trials.njk: -------------------------------------------------------------------------------- 1 | {% if feature.originTrials.length %} 2 |
3 |

Origin trials

4 |

Origin trials let you try experimental features on your production website and give feedback to browser vendors. This feature currently has the following origin trials available:

5 | 12 |
13 | {% endif %} -------------------------------------------------------------------------------- /site/widely-available.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Widely available 3 | layout: layout.njk 4 | --- 5 | 6 |
7 | {% include "feature_list_views.njk" %} 8 | 9 |

{{ title }}

10 | 11 |

These features have been supported across all browsers for at least 30 months, making them Baseline widely available. To learn more, see Baseline.

12 | 13 | 20 |
21 | -------------------------------------------------------------------------------- /site/_includes/discouraged.njk: -------------------------------------------------------------------------------- 1 | {% if feature.discouraged %} 2 |
3 |

4 | This feature is discouraged. 5 | {% if feature.discouraged.alternatives %} 6 | Consider using 7 | {% for alternative in feature.discouraged.alternatives %} 8 | {{ allFeaturesAsObject[alternative].name }} 9 | {% if not loop.last %}, {% endif %} 10 | {% endfor %} 11 | instead. 12 | {% endif %} 13 | For the rationale, see: 14 |

15 | 20 |
21 | {% endif %} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "web-features-explorer", 4 | "description": "Visualize web features", 5 | "devDependencies": { 6 | "@11ty/eleventy": "^3.1.2", 7 | "@11ty/eleventy-plugin-rss": "^2.0.4", 8 | "@ddbeck/mdn-content-inventory": "^0.2.20251220", 9 | "@mdn/browser-compat-data": "^7.2.2", 10 | "apexcharts": "^5.3.6", 11 | "browser-specs": "^4.64.0", 12 | "glob": "^13.0.0", 13 | "pagefind": "^1.4.0", 14 | "playwright": "^1.57.0", 15 | "web-features": "next", 16 | "yaml": "^2.8.2" 17 | }, 18 | "scripts": { 19 | "build": "npx rimraf docs && npx @11ty/eleventy", 20 | "serve": "npx @11ty/eleventy --serve", 21 | "pagefind": "npx pagefind --site docs --glob \"features/**/*.html\"" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /check-groups.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import { features } from "web-features"; 3 | 4 | const FILE = 'site/_data/featureCatalog.yml'; 5 | 6 | async function main() { 7 | const content = await fs.readFile(FILE, 'utf8'); 8 | const lines = content.split('\n'); 9 | 10 | lines.forEach((line, i) => { 11 | if (line.trim().startsWith("- ")) { 12 | // It's a feature. 13 | const idToCheck = line.trim().substring(2); 14 | if (!features[idToCheck]) { 15 | console.warn(`Feature ${idToCheck} was not found.`); 16 | } 17 | } 18 | }); 19 | 20 | for (const id in features) { 21 | if (!content.includes(`- ${id}\r\n`)) { 22 | console.warn(`Feature ${id} is missing from the list of groups`); 23 | } 24 | } 25 | } 26 | 27 | main(); 28 | -------------------------------------------------------------------------------- /site/one-missing-engine.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Features missing in just one browser engine 3 | layout: layout.njk 4 | --- 5 | 6 |
7 | {% include "feature_list_views.njk" %} 8 | 9 |

{{ title }}

10 | 11 |

The following features are missing from just one browser engine.
12 | This view shows the most recently updated features first. To see the features that have been blocked for the longest time first, see Features missing in just one browser engine (oldest first).

13 | 14 | 21 |
22 | -------------------------------------------------------------------------------- /site/one-missing-engine-oldest.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Features missing in just one browser engine (oldest first) 3 | layout: layout.njk 4 | --- 5 | 6 |
7 | {% include "feature_list_views.njk" %} 8 | 9 |

{{ title }}

10 | 11 |

The following features are missing from just one browser engine.
12 | This view shows the features that have been blocked for the longest time first. To see the most recently updated features first, see Features missing in just one browser engine.

13 | 14 | 21 |
22 | -------------------------------------------------------------------------------- /.github/workflows/update-wpt.yml: -------------------------------------------------------------------------------- 1 | name: Update WPT data 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check-out the repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Install dependencies 17 | run: npm ci 18 | 19 | - name: Run the update-wpt script 20 | run: node additional-data/scripts/update-wpt.js 21 | 22 | - name: Commit changes 23 | run: | 24 | git config --local user.email "${{ github.actor }}@users.noreply.github.com" 25 | git config --local user.name "${{ github.actor }}" 26 | git add . 27 | git commit -m "Refresh WPT data" --allow-empty 28 | git push origin main 29 | -------------------------------------------------------------------------------- /additional-data/scripts/update-interop.js: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | 4 | const INPUT_URL = "https://raw.githubusercontent.com/web-platform-tests/interop/refs/heads/main/web-features.json"; 5 | const OUTPUT_FILE = path.join(import.meta.dirname, "../interop.json"); 6 | 7 | async function main() { 8 | // Fetch the input data. 9 | console.log(`Fetching data from ${INPUT_URL}`); 10 | const response = await fetch(INPUT_URL); 11 | if (!response.ok) { 12 | throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); 13 | } 14 | const data = await response.json(); 15 | 16 | // Write the data to the output file. 17 | console.log(`Writing the data to ${OUTPUT_FILE}`); 18 | await fs.writeFile(OUTPUT_FILE, JSON.stringify(data, null, 2)); 19 | } 20 | 21 | main(); 22 | -------------------------------------------------------------------------------- /.github/workflows/update-interop.yml: -------------------------------------------------------------------------------- 1 | name: Update Interop data 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * 0' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check-out the repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Install dependencies 17 | run: npm ci 18 | 19 | - name: Run the update-interop script 20 | run: node additional-data/scripts/update-interop.js 21 | 22 | - name: Commit changes 23 | run: | 24 | git config --local user.email "${{ github.actor }}@users.noreply.github.com" 25 | git config --local user.name "${{ github.actor }}" 26 | git add . 27 | git commit -m "Refresh Interop data" --allow-empty 28 | git push origin main 29 | -------------------------------------------------------------------------------- /.github/workflows/update-origin-trials.yml: -------------------------------------------------------------------------------- 1 | name: Update origin trials 2 | 3 | on: 4 | schedule: 5 | - cron: '0 12 * * 1' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check-out the repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Install dependencies 17 | run: npm ci 18 | 19 | - name: Run the update-origin-trials script 20 | run: node additional-data/scripts/update-origin-trials.js 21 | 22 | - name: Commit changes 23 | run: | 24 | git config --local user.email "${{ github.actor }}@users.noreply.github.com" 25 | git config --local user.name "${{ github.actor }}" 26 | git add . 27 | git commit -m "Refresh OTs" --allow-empty 28 | git push origin main 29 | -------------------------------------------------------------------------------- /.github/workflows/update-standard-positions.yml: -------------------------------------------------------------------------------- 1 | name: Update standard positions 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * 0' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check-out the repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Install dependencies 17 | run: npm ci 18 | 19 | - name: Run the update-standard-positions script 20 | run: node update-timeline-stats.js 21 | 22 | - name: Commit changes 23 | run: | 24 | git config --local user.email "${{ github.actor }}@users.noreply.github.com" 25 | git config --local user.name "${{ github.actor }}" 26 | git add . 27 | git commit -m "Refresh standard positions" --allow-empty 28 | git push origin main 29 | -------------------------------------------------------------------------------- /.github/workflows/update-use-counters.yml: -------------------------------------------------------------------------------- 1 | name: Update Use Counters data 2 | 3 | on: 4 | schedule: 5 | - cron: '0 5 */7 * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check-out the repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Install dependencies 17 | run: npm ci 18 | 19 | - name: Run the update-use-counters script 20 | run: node additional-data/scripts/update-use-counters.js 21 | 22 | - name: Commit changes 23 | run: | 24 | git config --local user.email "${{ github.actor }}@users.noreply.github.com" 25 | git config --local user.name "${{ github.actor }}" 26 | git add . 27 | git commit -m "Refresh Use Counters data" --allow-empty 28 | git push origin main 29 | -------------------------------------------------------------------------------- /.github/workflows/update-state-of-surveys.yml: -------------------------------------------------------------------------------- 1 | name: Update State Of survey results 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 1 */2 *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check-out the repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Install dependencies 17 | run: npm ci 18 | 19 | - name: Run the update-state-of script 20 | run: node additional-data/scripts/update-state-of.js 21 | 22 | - name: Commit changes 23 | run: | 24 | git config --local user.email "${{ github.actor }}@users.noreply.github.com" 25 | git config --local user.name "${{ github.actor }}" 26 | git add . 27 | git commit -m "Refresh State Of survey results" --allow-empty 28 | git push origin main 29 | -------------------------------------------------------------------------------- /site/assets/edge.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/browser.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layout.njk 3 | pagination: 4 | data: browsers 5 | size: 1 6 | alias: browser 7 | permalink: "browsers/{{ browser.id | slugify }}/" 8 | --- 9 | 10 |
11 | {% include "feature_list_views.njk" %} 12 | 13 | {% for release in browser.releases %} 14 | {% if release.status == "current" %} 15 |

New features in {{ browser.name }} {{ release.version }} (released on {{ release.date }})

16 | 17 | 26 | {% endif %} 27 | {% endfor %} 28 | 29 |
30 | -------------------------------------------------------------------------------- /site/ids.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Feature IDs and BCD keys 3 | layout: layout.njk 4 | --- 5 | 6 |
7 |

{{ title }}

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% for feature in allFeatures %} 20 | 21 | 22 | 23 | 24 | 31 | 32 | {% endfor %} 33 | 34 |
IDNameDescriptionBCD keys
{{ feature.id }}{% prettyFeatureName feature.name %}{{ feature.description }} 25 |
    26 | {% for key in feature.compat_features %} 27 |
  • {{ key }}
  • 28 | {% endfor %} 29 |
30 |
35 | 36 |
37 | -------------------------------------------------------------------------------- /site/limited-availability.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Limited availability 3 | layout: layout.njk 4 | --- 5 | 6 |
7 | {% include "feature_list_views.njk" %} 8 | 9 |

{{ title }}

10 | 11 |

These features are not yet available across all browsers.

12 |

Consider the browser support data below as well as your users' browser versions before using these features in production. Provide your essential content and functionality to as many users as possible by using progressive enhancements.

13 | 14 | 21 |
22 | -------------------------------------------------------------------------------- /additional-data/scripts/update-wpt.js: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | 4 | const INPUT_URL = "https://raw.githubusercontent.com/captainbrosset/wpt-to-web-features/refs/heads/main/wpt-web-features.json"; 5 | const OUTPUT_FILE = path.join(import.meta.dirname, "../wpt.json"); 6 | 7 | async function main() { 8 | // Fetch the input data. 9 | console.log(`Fetching data from ${INPUT_URL}`); 10 | const response = await fetch(INPUT_URL); 11 | if (!response.ok) { 12 | throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); 13 | } 14 | const data = await response.json(); 15 | 16 | // We only care about the keys. 17 | const webFeaturesThatHaveWPTs = Object.keys(data); 18 | 19 | // Write the data to the output file. 20 | console.log(`Writing the data to ${OUTPUT_FILE}`); 21 | await fs.writeFile(OUTPUT_FILE, JSON.stringify(webFeaturesThatHaveWPTs, null, 2)); 22 | } 23 | 24 | main(); 25 | -------------------------------------------------------------------------------- /site/_includes/feature_list_views.njk: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/_includes/docs.njk: -------------------------------------------------------------------------------- 1 |
2 |

MDN documentation

3 | {% if feature.mdnUrls.length > 0 %} 4 | 17 | {% else %} 18 | No MDN documentation found. 19 | {% if not feature.discouraged %} 20 | You can search for the feature on MDN. If you believe that MDN has no documentation about this feature, you can open an issue on MDN's GitHub repository. 21 | {% endif %} 22 | {% endif %} 23 |
-------------------------------------------------------------------------------- /site/_includes/compat_full.njk: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/newly-available.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Newly available 3 | layout: layout.njk 4 | --- 5 | 6 |
7 | {% include "feature_list_views.njk" %} 8 | 9 |

{{ title }}

10 | 11 |

These features recently became available across all browsers, making them Baseline newly available. To learn more, see Baseline.

12 |

Consider the browser support data below as well as your users' browser versions before using these features in production. Provide your essential content and functionality to as many users as possible by using progressive enhancements.

13 | 14 | 21 |
22 | -------------------------------------------------------------------------------- /site/assets/safari.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/index.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | layout: layout.njk 4 | --- 5 | 6 |
7 |

Stay up-to-date with the web platform

8 |

Use the Web platform features explorer to discover new features and APIs and stay up-to-date with changes. 9 | 10 |

11 |

Newly available across browsers (RSS feed)

12 | 20 |
21 | 22 |
23 |

Now widely available across browsers (RSS feed)

24 | 32 |
33 |
-------------------------------------------------------------------------------- /site/unordinary-feature.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layout.njk 3 | pagination: 4 | data: allUnordinaryFeatures 5 | size: 1 6 | alias: feature 7 | permalink: "features/{{ feature.id | slugify }}/" 8 | eleventyComputed: 9 | title: "{{ feature.id }}" 10 | --- 11 | 12 |
13 |
14 | {% if feature.kind == "moved" %} 15 |
16 |

This feature has moved

17 |
18 |

You're being redirected to {% prettyFeatureName allFeaturesAsObject[feature.redirect_target].name %} instead. 19 |

20 | 21 | {% elif feature.kind == "split" %} 22 |
23 |

This feature has been split

24 |
25 |

See the following features instead:

26 | 33 | {% endif %} 34 |
35 |
36 | -------------------------------------------------------------------------------- /site/_includes/layout.njk: -------------------------------------------------------------------------------- 1 | --- 2 | siteTitle: Web platform features explorer 3 | --- 4 | 5 | 6 | 7 | 8 | {{ siteTitle }} - {{ title }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | {{ siteTitle }} 18 | 19 | 31 | 32 | 33 |
34 | 35 | {{ content | safe }} 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /.github/workflows/generate-site.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy site 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | schedule: 7 | - cron: '0 0 * * *' 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Check-out the repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Install dependencies 19 | if: ${{ github.event_name == 'push' }} 20 | run: npm ci 21 | 22 | - name: Install dependency updates 23 | if: ${{ github.event_name != 'push' }} 24 | run: | 25 | npx npm-check-updates -u 26 | npm install 27 | npm update web-features 28 | 29 | - name: Update timeline stats 30 | run: node update-timeline-stats.js 31 | 32 | - name: Commit dependency changes 33 | if: ${{ github.event_name != 'push' }} 34 | run: | 35 | git config --local user.email "${{ github.actor }}@users.noreply.github.com" 36 | git config --local user.name "${{ github.actor }}" 37 | git add . 38 | git commit -m "Bump deps" --allow-empty 39 | git push origin main 40 | 41 | - name: Generate site 42 | run: npm run build 43 | 44 | - name: Generate pagefind index 45 | run: npm run pagefind 46 | 47 | - name: Deploy to GitHub Pages 48 | uses: JamesIves/github-pages-deploy-action@v4 49 | with: 50 | folder: docs 51 | -------------------------------------------------------------------------------- /site/assets/firefox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /site/bcd-mapping.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mapping of the BCD keys in web-features 3 | layout: layout.njk 4 | --- 5 | 6 | 30 |
31 |

{{ title }}

32 | 33 | 38 | 39 |

Unmapped BCD keys

40 | 41 | 42 | 43 | 48 |
49 | 50 | 57 | -------------------------------------------------------------------------------- /site/browse.njk: -------------------------------------------------------------------------------- 1 | ---js 2 | const title = "Browse web platform features"; 3 | const layout = "layout.njk" 4 | 5 | function isGroup(item) { 6 | return !Array.isArray(item); 7 | } 8 | --- 9 | 10 | {% macro recurse(parent) %} 11 | 40 | {% endmacro %} 41 | 42 |
43 |

{{ title }}

44 | 45 |

Browse the entire list of available web platform features hierarchically below.

46 | 47 |
48 | {{ recurse(featureCatalog) }} 49 |
50 |
51 | -------------------------------------------------------------------------------- /site/groups.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Groups 3 | layout: layout.njk 4 | --- 5 | 6 | {% macro groupMacro(id, group, allGroups) %} 7 |
  • 8 |
    9 | {{ group.name }} 10 | 17 | 26 |
    27 |
  • 28 | {% endmacro %} 29 | 30 |
    31 |

    {{ title }}

    32 | 33 |

    Work in progress. This page displays groups of features in a tree structure. Groups have not yet been finalized, expect frequent changes.

    34 | 35 | 42 | 43 |

    Ungrouped features

    44 | 51 |
    52 | -------------------------------------------------------------------------------- /site/assets/timeline-durations.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "year": "2016", 4 | "min": 238, 5 | "q1": 531, 6 | "median": 776, 7 | "q3": 1085, 8 | "max": 2679, 9 | "nb": 25 10 | }, 11 | { 12 | "year": "2017", 13 | "min": 125, 14 | "q1": 590.75, 15 | "median": 1025.5, 16 | "q3": 1671.25, 17 | "max": 4811, 18 | "nb": 30 19 | }, 20 | { 21 | "year": "2018", 22 | "min": 246, 23 | "q1": 808.5, 24 | "median": 1379, 25 | "q3": 2046, 26 | "max": 4690, 27 | "nb": 19 28 | }, 29 | { 30 | "year": "2019", 31 | "min": 524, 32 | "q1": 666, 33 | "median": 1123.5, 34 | "q3": 1778.5, 35 | "max": 2956, 36 | "nb": 10 37 | }, 38 | { 39 | "year": "2020", 40 | "min": 50, 41 | "q1": 801.75, 42 | "median": 1568.5, 43 | "q3": 2134.75, 44 | "max": 5545, 45 | "nb": 116 46 | }, 47 | { 48 | "year": "2021", 49 | "min": 41, 50 | "q1": 496.5, 51 | "median": 1119, 52 | "q3": 1724.5, 53 | "max": 3484, 54 | "nb": 43 55 | }, 56 | { 57 | "year": "2022", 58 | "min": 34, 59 | "q1": 249.25, 60 | "median": 1285, 61 | "q3": 2233, 62 | "max": 4841, 63 | "nb": 46 64 | }, 65 | { 66 | "year": "2023", 67 | "min": 25, 68 | "q1": 447, 69 | "median": 1155, 70 | "q3": 2071, 71 | "max": 7165, 72 | "nb": 81 73 | }, 74 | { 75 | "year": "2024", 76 | "min": 42, 77 | "q1": 274.75, 78 | "median": 378.5, 79 | "q3": 1410.25, 80 | "max": 7251, 81 | "nb": 54 82 | }, 83 | { 84 | "year": "2025", 85 | "min": 56, 86 | "q1": 426, 87 | "median": 871.5, 88 | "q3": 1684.5, 89 | "max": 5579, 90 | "nb": 32 91 | } 92 | ] -------------------------------------------------------------------------------- /site/newly-available-feed.njk: -------------------------------------------------------------------------------- 1 | ---json 2 | { 3 | "permalink": "/newly-available.xml", 4 | "eleventyExcludeFromCollections": true, 5 | "metadata": { 6 | "title": "Baseline Newly Available Features", 7 | "subtitle": "Features of the web platform which became Baseline Newly Available.", 8 | "language": "en", 9 | "url": "https://web-platform-dx.github.io/web-features-explorer/", 10 | "author": { 11 | "name": "WebDX Community Group" 12 | } 13 | } 14 | } 15 | --- 16 | 17 | 18 | {{ metadata.title }} 19 | {{ metadata.subtitle }} 20 | 21 | 22 | {{ newlyAvailableFeatures[0].baselineLowDateAsObject | dateToRfc3339 }} 23 | {{ metadata.url }} 24 | 25 | {{ metadata.author.name }} 26 | 27 | 28 | {% for feature in newlyAvailableFeatures %} 29 | 30 | {% prettyFeatureName feature.name %} 31 | 32 | {{ feature.baselineLowDateAsObject | dateToRfc3339 }} 33 | {{ metadata.url + 'features/' + feature.id | slugify }} 34 | {% prettyFeatureName feature.name %} 36 |

    {{ feature.description_html | safe }}

    37 | ]]>
    38 |
    39 | {% endfor %} 40 |
    41 | -------------------------------------------------------------------------------- /site/widely-available-feed.njk: -------------------------------------------------------------------------------- 1 | ---json 2 | { 3 | "permalink": "/widely-available.xml", 4 | "eleventyExcludeFromCollections": true, 5 | "metadata": { 6 | "title": "Baseline Widely Available Features", 7 | "subtitle": "Features of the web platform which became Baseline Widely Available.", 8 | "language": "en", 9 | "url": "https://web-platform-dx.github.io/web-features-explorer/", 10 | "author": { 11 | "name": "WebDX Community Group" 12 | } 13 | } 14 | } 15 | --- 16 | 17 | 18 | {{ metadata.title }} 19 | {{ metadata.subtitle }} 20 | 21 | 22 | {{ widelyAvailableFeatures[0].baselineHighDateAsObject | dateToRfc3339 }} 23 | {{ metadata.url }} 24 | 25 | {{ metadata.author.name }} 26 | 27 | 28 | {% for feature in widelyAvailableFeatures %} 29 | 30 | {% prettyFeatureName feature.name %} 31 | 32 | {{ feature.baselineHighDateAsObject | dateToRfc3339 }} 33 | {{ metadata.url + 'features/' + feature.id | slugify }} 34 | {% prettyFeatureName feature.name %} 36 |

    {{ feature.description_html | safe }}

    37 | ]]>
    38 |
    39 | {% endfor %} 40 |
    41 | -------------------------------------------------------------------------------- /site/assets/search.js: -------------------------------------------------------------------------------- 1 | addEventListener("DOMContentLoaded", () => { 2 | function debounce(func, delay) { 3 | let timer; 4 | return function (...args) { 5 | clearTimeout(timer); 6 | timer = setTimeout(() => { 7 | func.apply(this, args); 8 | }, delay); 9 | }; 10 | } 11 | 12 | const searchInput = document.querySelector("#search"); 13 | const searchResults = document.querySelector("output"); 14 | 15 | searchInput.addEventListener( 16 | "focus", 17 | () => { 18 | // Use dynamic import to load /pagefind/pagefind.js 19 | import("/web-features-explorer/pagefind/pagefind.js").then((pagefind) => { 20 | pagefind.init(); 21 | 22 | searchInput.addEventListener("input", debounce(async (event) => { 23 | searchResults.innerHTML = ""; 24 | 25 | if (event.target.value.length < 2) { 26 | return; 27 | } 28 | 29 | const search = await pagefind.search(event.target.value); 30 | 31 | if (search.results.length === 0) { 32 | return; 33 | } 34 | 35 | const ul = document.createElement("ul"); 36 | 37 | let counter = 0; 38 | for (const result of search.results) { 39 | counter ++; 40 | if (counter > 10) { 41 | break; 42 | } 43 | 44 | const resultData = await result.data(); 45 | 46 | const li = document.createElement("li"); 47 | 48 | const a = document.createElement("a"); 49 | a.href = resultData.url; 50 | 51 | const h3 = document.createElement("h3"); 52 | h3.textContent = resultData.meta.title; 53 | 54 | const p = document.createElement("p"); 55 | p.innerHTML = resultData.excerpt; 56 | 57 | a.appendChild(h3); 58 | a.appendChild(p); 59 | li.appendChild(a); 60 | ul.appendChild(li); 61 | } 62 | 63 | searchResults.appendChild(ul); 64 | }, 500)); 65 | }); 66 | }, 67 | { once: true } 68 | ); 69 | }); 70 | -------------------------------------------------------------------------------- /site/about.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: About this website 3 | layout: layout.njk 4 | --- 5 | 6 |
    7 |

    {{ title }}

    8 | 9 |

    The Web platform features explorer website is a project from the W3C WebDX Community Group (WebDX CG). Its goal is to provide a visualization of the data from the web-features repository, which the WebDX CG maintains.

    10 | 11 |

    In addition to displaying the web-features data, this website also links the data to other resources, such as browser-compat-data, MDN documentation, specifications, vendor positions, origin trials, and more.

    12 | 13 |

    The website also displays Baseline information for each web feature. To learn more about Baseline, see What is Baseline?.

    14 | 15 |

    The website's content was last generated on {{ versions.date }} using web-features {{ versions.webFeatures }} and browser-compat-data {{ versions.bcd }}. The source code of the website is at web-features-explorer GitHub repo.

    16 | 17 |

    Other pages

    18 | 19 |

    Here are a few other pages on this website that might be of interest. They are not in the top-level navigation menu because they are either work in progress or just experiments.

    20 | 27 |
    28 | -------------------------------------------------------------------------------- /site/filter.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Client-side filtering features 3 | layout: layout.njk 4 | --- 5 | 6 |
    7 |

    {{ title }}

    8 | 9 | 18 | 19 | 30 | 31 | 32 | 33 | 86 |
    87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web platform features explorer 2 | 3 | The web platform features explorer website visualizes the web platform data that's maintained in the [web-platform-dx/web-features](https://github.com/web-platform-dx/web-features/) repository. 4 | 5 | View the live website at https://web-platform-dx.github.io/web-features-explorer/. 6 | 7 | ## Goals of the website 8 | 9 | * To test and visualize the data that's in the web-features repo. 10 | * To connect the data to other relevant sources of information about web features. 11 | * To provide web developers with useful ways to stay up to date with the web and explore features. 12 | 13 | ## Architecture 14 | 15 | ### Data 16 | 17 | The data that the website is based on comes from the [web-features](https://www.npmjs.com/package/web-features) NPM package. The site uses the **next** version if the package, which provide the data from the latest commit on the web-features repo. 18 | 19 | In addition, the site uses the [@mdn/browser-compat-data](https://www.npmjs.com/package/browser-compat-data) NPM package to get various other pieces of information, such as links to MDN documentation and links to bug trackers. 20 | 21 | ### Pages 22 | 23 | The web pages are built by using the [Eleventy static site generator](https://www.11ty.dev/). 24 | 25 | ## Local development 26 | 27 | To run the website locally, clone the repository, make sure the dependencies are updated, and then build the site. 28 | 29 | ### Update the dependencies 30 | 31 | To ensure you have the latest data: 32 | 33 | 1. Run `npx npm-check-updates -u` 34 | 35 | 1. Run `npm update web-features` 36 | 37 | 1. `npm install` 38 | 39 | ### Build the site 40 | 41 | To build the site: 42 | 43 | 1. Run `npm run build` to generate the site 44 | 45 | 1. Check the `docs` folder for the resulting build files. 46 | 47 | The source template files used to build the site are in the `site` folder. 48 | 49 | ### Run and edit the site locally 50 | 51 | To run the website on a local development server: 52 | 53 | 1. Run `npm run serve`. 54 | 55 | 1. Open a web browser and go to `http://localhost:8080`. 56 | 57 | 1. Modify a source file, wait for the build to run automatically, and for the changes to appear in the browser. 58 | 59 | ## Production environment 60 | 61 | The website is deployed to production using [GitHub Pages](https://pages.github.com/). The static HTML pages are generated in the [gh-pages branch](https://github.com/web-platform-dx/web-features-explorer/tree/gh-pages) on a regular basis by the GitHub Actions script found in `.github/workflows/generate-site.yaml`. 62 | 63 | The dependencies are also automatically updated every day by using the GitHub Actions script in `.github/workflows/bump-deps.yaml`. 64 | -------------------------------------------------------------------------------- /site/monthly-updates.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layout.njk 3 | pagination: 4 | data: perMonth 5 | size: 1 6 | alias: month 7 | permalink: "release-notes/{{ month.date | slugify }}/" 8 | eleventyComputed: 9 | title: "{{ month.date }} release notes" 10 | --- 11 | 12 |
    13 |

    📃 {{ month.date }} release notes

    14 | 15 |
    16 | {% if month.features.low.length > 0 %} 17 |
    18 |

    Newly available

    19 |

    The following features are newly available:

    20 | 28 |
    29 | {% endif %} 30 | 31 | {% if month.features.high.length > 0 %} 32 |
    33 |

    Widely available

    34 |

    The following features are now widely available:

    35 | 43 |
    44 | {% endif %} 45 | 46 | {% for browser in browsers %} 47 | {% if month.features[browser.id].length > 0 %} 48 |
    49 |

    New in {{ browser.name }}

    50 |

    The following features are now available in {{ browser.name }}:

    51 | 59 |
    60 | {% endif %} 61 | {% endfor %} 62 |
    63 | 64 | 78 |
    79 | -------------------------------------------------------------------------------- /site/_includes/feature_full.njk: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    {% prettyFeatureName feature.name %}

    4 | 5 | 15 |
    16 | 17 | {% include "discouraged.njk" %} 18 | 19 |

    20 | {{ feature.description_html | safe }} 21 |

    22 | 23 |
    24 |

    Status

    25 | {% include "baseline.njk" %} 26 | {% if feature.expectedBaselineHighDate %} 27 |

    This feature is expected to reach Baseline Widely Available status on: {{ feature.expectedBaselineHighDate }} 28 | {% endif %} 29 | {% if not feature.status.baseline and feature.hasNegativeStandardPosition %} 30 |

    This feature received a negative standards position from at least one browser vendor. It's very unlikely to ever become Baseline in this current form.

    31 | {% endif %} 32 |
    33 | 34 | {% include "docs.njk" %} 35 | 36 | {% include "specs.njk" %} 37 | 38 |
    39 |

    Browser support{% if feature.caniuse %} (view on caniuse.com){% endif %}

    40 | {% include "compat_full.njk" %} 41 | 42 | {%- if feature.blockedOn %} 43 |

    Baseline availability blocked since {{ feature.blockedSince }} by {{ feature.blockedOn }} ({{ feature.monthsBlocked }} months)

    44 | {%- endif %} 45 |
    46 | 47 | {% include "origin_trials.njk" %} 48 | 49 | {% include "surveys.njk" %} 50 | 51 | {% include "use_counters.njk" %} 52 | 53 | {% include "interop.njk" %} 54 | 55 | {% include "wpt.njk" %} 56 | 57 | 68 |
    69 | -------------------------------------------------------------------------------- /additional-data/scripts/check-docs.js: -------------------------------------------------------------------------------- 1 | // Run this script to add new feature IDs into the mdn-docs.json file. 2 | // This script is not run automatically yet. Run it manually when you want 3 | // to make sure the mdn-docs.json file contains the full list of features. 4 | // This script does not map features to MDN URLs on its own. Mapping MDN URLs 5 | // is done manually. 6 | 7 | import { features } from "web-features"; 8 | import bcd from "@mdn/browser-compat-data" with { type: "json" }; 9 | import * as fs from "fs/promises"; 10 | import mdnDocsOverrides from "../mdn-docs.json" with { type: "json" }; 11 | import path from "path"; 12 | 13 | const FILE = path.join(import.meta.dirname, "../mdn-docs.json"); 14 | 15 | function getFeaturesMdnUrls() { 16 | const output = {}; 17 | 18 | for (const id in features) { 19 | const feature = features[id]; 20 | const urls = []; 21 | 22 | const keys = feature.compat_features; 23 | if (keys && keys.length) { 24 | for (const key of keys) { 25 | const keyParts = key.split("."); 26 | 27 | let data = bcd; 28 | for (const part of keyParts) { 29 | if (!data || !data[part]) { 30 | console.warn( 31 | `No BCD data for ${key}. Check if the web-features and browser-compat-data dependencies are in sync.` 32 | ); 33 | continue; 34 | } 35 | data = data[part]; 36 | } 37 | 38 | let url = data?.__compat?.mdn_url; 39 | if (url) { 40 | url = url.replace("https://developer.mozilla.org/docs/", ""); 41 | urls.push(url); 42 | } 43 | } 44 | } 45 | 46 | if (urls.length) { 47 | output[id] = urls; 48 | } 49 | } 50 | 51 | return output; 52 | } 53 | 54 | async function main() { 55 | const featuresMdnUrls = getFeaturesMdnUrls(); 56 | 57 | for (const id in features) { 58 | // If there's already an override, skip. 59 | if (mdnDocsOverrides[id] && mdnDocsOverrides[id].length) { 60 | continue; 61 | } 62 | 63 | // No override found, let's see if we have a single MDN URL for this feature. 64 | if (featuresMdnUrls[id] && featuresMdnUrls[id].length === 1) { 65 | // Yes, let's add it to the data. 66 | mdnDocsOverrides[id] = [featuresMdnUrls[id][0]]; 67 | } else { 68 | // Otherwise, add the feature ID to the data with an empty array. 69 | mdnDocsOverrides[id] = []; 70 | } 71 | 72 | // Report special cases 73 | // if (featuresMdnUrls[id] && featuresMdnUrls[id].length === 2) { 74 | // console.log('---'); 75 | // console.log(id); 76 | // console.log(` "${featuresMdnUrls[id]}"`); 77 | // } 78 | } 79 | 80 | // Sort mdnDocsOverrides by key. 81 | const orderedDocOverrides = {}; 82 | Object.keys(mdnDocsOverrides) 83 | .sort() 84 | .forEach(function (key) { 85 | orderedDocOverrides[key] = mdnDocsOverrides[key]; 86 | }); 87 | 88 | // Write the JSON file back to disk. 89 | const str = JSON.stringify(orderedDocOverrides, null, 2); 90 | await fs.writeFile(FILE, str); 91 | } 92 | 93 | main(); 94 | -------------------------------------------------------------------------------- /site/monthly-updates-feed.njk: -------------------------------------------------------------------------------- 1 | ---json 2 | { 3 | "permalink": "/release-notes.xml", 4 | "eleventyExcludeFromCollections": true, 5 | "metadata": { 6 | "title": "Web Platform Monthly Updates", 7 | "subtitle": "Monthly updates on the web platform features.", 8 | "language": "en", 9 | "url": "https://web-platform-dx.github.io/web-features-explorer/", 10 | "author": { 11 | "name": "WebDX Community Group" 12 | } 13 | } 14 | } 15 | --- 16 | 17 | 18 | {{ metadata.title }} 19 | {{ metadata.subtitle }} 20 | 21 | 22 | {{ collections.perMonth | getNewestCollectionItemDate | dateToRfc3339 }} 23 | {{ metadata.url }} 24 | 25 | {{ metadata.author.name }} 26 | 27 | {%- for month in perMonth %} 28 | {# current month is not stable, skip it #} 29 | {%- if month.isStableMonth %} 30 | {%- set absoluteMonthUrl = metadata.url + "release-notes/" + month.date | slugify + "/" %} 31 | 32 | {{ month.date }} web platform update 33 | 34 | {{ month.absoluteDate | dateToRfc3339 }} 35 | {{ absoluteMonthUrl }} 36 | {{ month.date }} web platform update 38 | 39 |
    40 | {% if month.features.low.length > 0 %} 41 |

    Newly available

    42 |

    The following features are newly available:

    43 | 51 | {% endif %} 52 | 53 | {% if month.features.high.length > 0 %} 54 |

    Widely available

    55 |

    The following features are now widely available:

    56 | 64 | {% endif %} 65 | 66 | {% for browser in browsers %} 67 | {% if month.features[browser.id].length > 0 %} 68 |

    New in {{ browser.name }}

    69 |

    The following features are now available in {{ browser.name }}:

    70 | 78 | {% endif %} 79 | {% endfor %} 80 |
    81 | ]]>
    82 |
    83 | {%- endif %} 84 | {%- endfor %} 85 |
    86 | -------------------------------------------------------------------------------- /additional-data/scripts/update-origin-trials.js: -------------------------------------------------------------------------------- 1 | // This script updates the origin-trials.json file with the latest Chrome Origin Trials data. 2 | 3 | // The format of the origin-trials.json file is the following: top-level keys are web-features IDs, 4 | // their values are arrays. 5 | // { 6 | // "": [] 7 | // } 8 | // The array for a feature is empty if there aren't any known origin trials for the feature. 9 | // Each known feature origin trial is an object as follows: 10 | // { 11 | // "browser": "", 12 | // "name": "", 13 | // "start": "", 14 | // "end": "", 15 | // "feedbackUrl": "", 16 | // "registrationUrl": "" 17 | // } 18 | 19 | // For Chrome, the script fetches the latest Chrome Origin Trials data by using the chromestatus.com 20 | // API, and updates the origin-trials.json file by mapping spec URLs between the features and the 21 | // OT. 22 | 23 | // Other browsers are not supported yet. 24 | // For Edge, see https://developer.microsoft.com/en-us/microsoft-edge/origin-trials/trials 25 | // For Firefox, see https://wiki.mozilla.org/Origin_Trials 26 | 27 | import { features } from "web-features"; 28 | import fs from "fs/promises"; 29 | import trials from "../origin-trials.json" with { type: "json" }; 30 | import path from "path"; 31 | 32 | const OUTPUT_FILE = path.join(import.meta.dirname, "../origin-trials.json"); 33 | 34 | async function getChromeAPIData(url) { 35 | const response = await fetch(url); 36 | const text = await response.text(); 37 | 38 | if (text.startsWith(")]}'")) { 39 | return JSON.parse(text.substring(5)); 40 | } 41 | return null; 42 | } 43 | 44 | async function getChromeOTs() { 45 | console.log("Retrieving all Chrome Origin Trials..."); 46 | const chromeOTsData = await getChromeAPIData("https://chromestatus.com/api/v0/origintrials"); 47 | 48 | const ots = []; 49 | 50 | if (chromeOTsData) { 51 | // For each OT in the fetched data. 52 | for (const ot of chromeOTsData.origin_trials) { 53 | // Check if the OT is still current. 54 | if (ot.status !== "ACTIVE") { 55 | continue; 56 | } 57 | 58 | // For each active OT, let's retrieve more data from chromestatus. 59 | const chromeStatusUrl = ot.chromestatus_url; 60 | const chromeStatusId = chromeStatusUrl.substring(chromeStatusUrl.lastIndexOf("/") + 1); 61 | console.log(`Retrieving the spec URL for Chrome OT ${ot.display_name}...`); 62 | const chromeStatusData = await getChromeAPIData(`https://chromestatus.com/api/v0/features/${chromeStatusId}`) 63 | if (!chromeStatusData) { 64 | continue; 65 | } 66 | 67 | ot.spec = chromeStatusData.spec_link; 68 | ots.push(ot); 69 | } 70 | } 71 | 72 | return ots; 73 | } 74 | 75 | function findChromeOTForFeature(featureId, chromeOTs) { 76 | const feature = features[featureId]; 77 | const specs = Array.isArray(feature.spec) ? feature.spec : [feature.spec]; 78 | 79 | for (const ot of chromeOTs) { 80 | // Currently, we match features to OTs by spec URL. 81 | // Later, it would be great if chromestatus entries had a mapping to web-feature IDs. 82 | if (specs.some(spec => spec === ot.spec)) { 83 | return { 84 | browser: "Chrome", 85 | name: ot.display_name, 86 | start: ot.start_milestone, 87 | end: ot.end_milestone, 88 | feedbackUrl: ot.feedback_url, 89 | registrationUrl: `https://developer.chrome.com/origintrials/#/register_trial/${ot.id}` 90 | }; 91 | } 92 | } 93 | return null; 94 | } 95 | 96 | async function main() { 97 | // Reset the trials data. 98 | // This makes sure we have the latest features, and that old and new trials are 99 | // removed and added. 100 | for (const id in features) { 101 | trials[id] = []; 102 | } 103 | 104 | const chromeOTs = await getChromeOTs(); 105 | 106 | for (const featureId in trials) { 107 | // We only support Chrome for now. 108 | const chromeOT = findChromeOTForFeature(featureId, chromeOTs); 109 | if (chromeOT) { 110 | console.log(`Adding Chrome Origin Trial for feature ${featureId}: ${chromeOT.name}`); 111 | trials[featureId].push(chromeOT); 112 | } 113 | } 114 | 115 | // Sort the trials by ID. 116 | const ordered = {}; 117 | Object.keys(trials) 118 | .sort() 119 | .forEach(function (id) { 120 | ordered[id] = trials[id]; 121 | }); 122 | 123 | // Store the updated trials back in the file. 124 | await fs.writeFile(OUTPUT_FILE, JSON.stringify(ordered, null, 2)); 125 | } 126 | 127 | main(); 128 | -------------------------------------------------------------------------------- /additional-data/scripts/update-state-of.js: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | import { execSync } from "child_process"; 4 | import { glob } from 'glob'; 5 | import { fileURLToPath } from 'url'; 6 | 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 8 | 9 | const REPO = "https://github.com/Devographics/surveys"; 10 | const TEMP_FOLDER = "surveys"; 11 | const OUTPUT_FILE = path.join(import.meta.dirname, "../state-of-surveys.json"); 12 | const SURVEYS_TO_INCLUDE = ["state_of_css", "state_of_html", "state_of_js"]; 13 | const SURVEY_DOMAINS = { 14 | state_of_css: "stateofcss.com/en-US", 15 | state_of_html: "stateofhtml.com/en-US", 16 | state_of_js: "stateofjs.com/en-US", 17 | }; 18 | const SURVEY_NAMES = { 19 | state_of_css: "State of CSS", 20 | state_of_html: "State of HTML", 21 | state_of_js: "State of JS", 22 | }; 23 | 24 | function extractWebFeatureReferences(data) { 25 | const webFeatureRefs = []; 26 | 27 | function walk(object, objectPath = []) { 28 | if (!object) { 29 | return; 30 | } 31 | 32 | if (Array.isArray(object)) { 33 | for (let i = 0; i < object.length; i++) { 34 | walk(object[i], [...objectPath, i]); 35 | } 36 | return; 37 | } 38 | 39 | if (typeof object !== "object") { 40 | return; 41 | } 42 | 43 | if (object.webFeature) { 44 | webFeatureRefs.push({ objectPath, object }); 45 | } 46 | 47 | for (const key in object) { 48 | walk(object[key], [...objectPath, key]); 49 | } 50 | } 51 | 52 | walk(data); 53 | 54 | return webFeatureRefs; 55 | } 56 | 57 | function extractSurveyTitleAndLink(path) { 58 | const survey = path[2]; 59 | const edition = path[3]; 60 | const year = edition.slice(-4); 61 | const question = path[4]; 62 | const subQuestion = path[5]; 63 | 64 | let link = `https://${year}.${SURVEY_DOMAINS[survey]}/${question}/#${subQuestion}` 65 | 66 | // Some quirks of State of surveys. 67 | link = link.replace("reading_list/reading_list", "features/reading_list"); 68 | link = link.replace("/reading_list/#reading_list", "/features/#reading_list"); 69 | link = link.replace("en-US/web_components/", "en-US/features/web_components/"); 70 | link = link.replace("en-US/mobile_web_apps", "en-US/features/mobile-web-apps"); 71 | link = link.replace("/interactivity", "/features/interactivity"); 72 | 73 | if (survey in SURVEY_DOMAINS) { 74 | return { 75 | name: `${SURVEY_NAMES[survey]} ${year}`, 76 | link, 77 | question, 78 | subQuestion, 79 | }; 80 | } 81 | 82 | return null; 83 | } 84 | 85 | async function main() { 86 | // Create a temp folder for the survey data. 87 | console.log(`Creating temporary folder`); 88 | const tempFolder = path.join(__dirname, TEMP_FOLDER); 89 | await fs.mkdir(tempFolder, { recursive: true }); 90 | 91 | // Clone the surveys repo. 92 | console.log(`Cloning ${REPO} into ${tempFolder}`); 93 | execSync(`git clone --depth 1 ${REPO} ${tempFolder}`); 94 | 95 | // Find all of the *.json files in sub-folders using glob. 96 | console.log(`Searching for JSON files in ${tempFolder}`); 97 | const files = glob.sync(`${tempFolder}/**/*.json`); 98 | 99 | const features = {}; 100 | for (const file of files) { 101 | if (!SURVEYS_TO_INCLUDE.some((survey) => file.includes(survey))) { 102 | continue; 103 | } 104 | 105 | let data = null; 106 | 107 | console.log(`Reading ${file}`); 108 | try { 109 | const content = await fs.readFile(file, "utf-8"); 110 | data = JSON.parse(content); 111 | } catch (e) { 112 | console.error(`Error reading ${file}: ${e.message}`); 113 | continue; 114 | } 115 | 116 | console.log(`Extracting web feature references from ${file}`); 117 | const refs = extractWebFeatureReferences(data); 118 | for (const ref of refs) { 119 | const { objectPath, object } = ref; 120 | const { name, link, question, subQuestion } = extractSurveyTitleAndLink(objectPath); 121 | const featureId = object.webFeature.id; 122 | 123 | if (!features[featureId]) { 124 | features[featureId] = []; 125 | } 126 | 127 | // Find if there's already a reference to the exact same survey link. 128 | if (features[featureId].some((ref) => ref.link === link)) { 129 | continue; 130 | } 131 | 132 | features[featureId].push({ name, link, question, subQuestion, path: objectPath.join(".") }); 133 | } 134 | } 135 | 136 | console.log("------------------"); 137 | console.log("Web feature references found in the survey data:"); 138 | console.log(features); 139 | 140 | // Delete the folder. 141 | console.log(`Deleting temporary folder ${tempFolder}`); 142 | await fs.rm(tempFolder, { recursive: true }); 143 | 144 | // Write the data to the output file. 145 | console.log(`Writing the data to ${OUTPUT_FILE}`); 146 | await fs.writeFile(OUTPUT_FILE, JSON.stringify(features, null, 2)); 147 | } 148 | 149 | main(); 150 | -------------------------------------------------------------------------------- /additional-data/scripts/update-use-counters.js: -------------------------------------------------------------------------------- 1 | // This script updates the use-counters.json file with the latest usage data about web features. 2 | 3 | // The use-counters.json file is structured as follows. 4 | // 5 | // Top-level keys are web-features IDs, and their values are objects with ... 6 | // { 7 | // "": { 8 | // ... 9 | // } 10 | // } 11 | // 12 | 13 | import { features } from "web-features"; 14 | import fs from "fs/promises"; 15 | import useCounters from "../use-counters.json" with { type: "json" }; 16 | import path from "path"; 17 | 18 | const OUTPUT_FILE = path.join(import.meta.dirname, "../use-counters.json"); 19 | 20 | // UC names can be derived from web-feature IDs, but not the other way around. 21 | // The chromestatus API we use to get the list of use-counters returns UC names. 22 | // So, this function first converts all web-feature IDs to expected UC names, 23 | // and then reverses the map. This way, when we query the API, we can lookup 24 | // the UC names the API returns and get the web-feature IDs from them. 25 | let UCNAMES_TO_WFIDS = {}; 26 | function prepareUseCounterMapping() { 27 | // See the rule which Chromium uses at: 28 | // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/public/mojom/use_counter/metrics/webdx_feature.mojom;l=35-47;drc=af140c76c416302ecadb5e7cf3f989d6293ba5ec 29 | // In short, uppercase the first letter in each sequence of letters and remove hyphens. 30 | Object.keys(features).forEach(id => { 31 | const expectedUCName = id 32 | .replace(/[a-z]+/g, (m) => m[0].toUpperCase() + m.substring(1)) 33 | .replaceAll("-", ""); 34 | UCNAMES_TO_WFIDS[expectedUCName] = id; 35 | }); 36 | } 37 | prepareUseCounterMapping(); 38 | 39 | async function getWebFeaturesThatMapToUseCounters() { 40 | // Get the latest chromium source file which contains all use counters. 41 | const response = await fetch("https://raw.githubusercontent.com/chromium/chromium/refs/heads/main/third_party/blink/public/mojom/use_counter/metrics/webdx_feature.mojom"); 42 | const sourceText = await response.text(); 43 | 44 | // Parse the source text to extract all use counters. 45 | // The lines that we are interested in look like this: 46 | // kSomeUseCounterName = number, 47 | const useCounterLines = sourceText.match(/k([A-Z][a-zA-Z0-9_]+) = (\d+),\n/g); 48 | if (!useCounterLines) { 49 | throw new Error("Failed to parse use counters from the source file."); 50 | } 51 | 52 | const ret = {}; 53 | 54 | for (const line of useCounterLines) { 55 | // Extract the use counter ID and name from the line. 56 | const match = line.match(/k([A-Z][a-zA-Z0-9_]+) = (\d+),/); 57 | if (!match) { 58 | console.warn(`Failed to parse line: ${line}`); 59 | continue; 60 | } 61 | const ucName = match[1]; 62 | const ucId = parseInt(match[2], 10); 63 | 64 | // Some useCounters are drafts. This happens when the 65 | // corresponding web-feature is not yet in web-features. 66 | // In theory, we ignore them, but we also check if there 67 | // is a feature by that name anyway. 68 | if (ucName.startsWith("DRAFT_")) { 69 | // Check, just in case, if the feature is now in web-features. 70 | const wfid = UCNAMES_TO_WFIDS[ucName.replace("DRAFT_", "")]; 71 | if (features[wfid]) { 72 | console.warn( 73 | `Use-counter ${ucName} is a draft, but the feature ${wfid} exists in web-features.` 74 | ); 75 | } 76 | 77 | console.log(`Ignoring use-counter: ${ucName} since it's a draft.`); 78 | continue; 79 | } 80 | 81 | // Some useCounters are obsolete. We ignore them. 82 | if (ucName.startsWith("OBSOLETE_")) { 83 | console.log(`Ignoring use-counter: ${ucName} since it's an obsolete counter.`); 84 | continue; 85 | } 86 | 87 | const webFeatureId = UCNAMES_TO_WFIDS[ucName]; 88 | if (!webFeatureId) { 89 | console.warn(`No web-feature ID found for use-counter: ${ucName}`); 90 | continue; 91 | } 92 | 93 | ret[webFeatureId] = { ucId, ucName }; 94 | } 95 | 96 | return ret; 97 | } 98 | 99 | async function getFeatureUsageInPercentageOfPageLoads(ucId) { 100 | const response = await fetch(`https://chromestatus.com/data/timeline/webfeaturepopularity?bucket_id=${ucId}`); 101 | const data = await response.json(); 102 | // The API returns data for a few months, but it seems like the older the data, the less granular it is. 103 | // We don't care much for historical data here, so just return the last day. 104 | return data.length ? data[data.length - 1].day_percentage : null; 105 | } 106 | 107 | async function main() { 108 | // First, add any missing feature IDs to the useCounters object. 109 | for (const id in features) { 110 | if (!useCounters[id]) { 111 | useCounters[id] = {}; 112 | } 113 | } 114 | 115 | // Now find the use-counters that map to web-features. 116 | const wfToUcMapping = await getWebFeaturesThatMapToUseCounters(); 117 | 118 | // For each, get the usage stats. 119 | for (const wfId in wfToUcMapping) { 120 | console.log(`Getting usage for ${wfId}...`); 121 | const { ucId } = wfToUcMapping[wfId]; 122 | const usage = await getFeatureUsageInPercentageOfPageLoads(ucId); 123 | if (usage) { 124 | useCounters[wfId] = { 125 | percentageOfPageLoad: usage, 126 | chromeStatusUrl: `https://chromestatus.com/metrics/webfeature/timeline/popularity/${ucId}` 127 | }; 128 | } 129 | } 130 | 131 | // Sort the useCounters by ID. 132 | const ordered = {}; 133 | Object.keys(useCounters) 134 | .sort() 135 | .forEach(function (id) { 136 | ordered[id] = useCounters[id]; 137 | }); 138 | 139 | // Store the updated positions back in the file. 140 | await fs.writeFile(OUTPUT_FILE, JSON.stringify(ordered, null, 2)); 141 | } 142 | 143 | main(); 144 | -------------------------------------------------------------------------------- /additional-data/scripts/update-standard-positions.js: -------------------------------------------------------------------------------- 1 | // This script updates the standard-positions.json file with the positions and concerns 2 | // found in the GitHub issues of the vendors that have a URL in the standard-positions.json file. 3 | 4 | // The standard-positions.json file is structured as follows. 5 | // 6 | // Top-level keys are web-features IDs, and their values are objects with two keys: "mozilla" and "webkit": 7 | // { 8 | // "": { 9 | // "mozilla": {} 10 | // "webkit": {} 11 | // } 12 | // } 13 | // 14 | // Each mozilla and webkit object can be one of the following: 15 | // - An empty object, which means that the feature has no vendor URL yet. 16 | // - An object like { "url": "", "position": "", "concerns": [] }, which 17 | // means that the feature has a vendor URL, and the position and concerns might be known. 18 | // - Optionally, the object can contain a "not" key with an array of issue URLs that should be ignored. 19 | // This is useful because we match the feature with the vendor issue by comparing the spec URLs, and 20 | // vendor issues might sometimes be about subparts of a spec that's relevant to another feature. 21 | 22 | // This script is not run automatically yet. Run it manually when you want to update the positions. 23 | // Always check the new position URLs added by the script (for mozilla only for now) to make sure they are correct. 24 | // Add "not" entries if any of the URLs are not relevant to a feature. 25 | 26 | import { features } from "web-features"; 27 | import fs from "fs/promises"; 28 | import positions from "../standard-positions.json" with { type: "json" }; 29 | import path from "path"; 30 | 31 | const OUTPUT_FILE = path.join(import.meta.dirname, "../standard-positions.json"); 32 | const MOZILLA_DATA_FILE = 33 | "https://raw.githubusercontent.com/mozilla/standards-positions/refs/heads/gh-pages/merged-data.json"; 34 | const WEBKIT_DATA_FILE = 35 | "https://raw.githubusercontent.com/WebKit/standards-positions/main/summary.json"; 36 | 37 | let mozillaData = null; 38 | let webkitData = null; 39 | 40 | async function getMozillaData() { 41 | if (!mozillaData) { 42 | const response = await fetch(MOZILLA_DATA_FILE); 43 | mozillaData = await response.json(); 44 | } 45 | 46 | return mozillaData; 47 | } 48 | 49 | async function getMozillaPosition(url) { 50 | const data = await getMozillaData(); 51 | 52 | const issueId = url.split("/").pop(); 53 | const issue = data[issueId]; 54 | if (!issue) { 55 | return { position: "", concerns: [] }; 56 | } 57 | 58 | return { 59 | position: issue.position || "", 60 | concerns: issue.concerns, 61 | }; 62 | } 63 | 64 | async function getWebkitData() { 65 | if (!webkitData) { 66 | const response = await fetch(WEBKIT_DATA_FILE); 67 | webkitData = await response.json(); 68 | } 69 | 70 | return webkitData; 71 | } 72 | 73 | async function getWebkitPosition(url) { 74 | const data = await getWebkitData(); 75 | 76 | for (const position of data) { 77 | if (position.id === url) { 78 | return { 79 | position: position.position || "", 80 | concerns: position.concerns || [], 81 | }; 82 | } 83 | } 84 | 85 | return { position: "", concerns: [] }; 86 | } 87 | 88 | async function getPosition(company, url) { 89 | switch (company) { 90 | case "webkit": 91 | return await getWebkitPosition(url); 92 | case "mozilla": 93 | return await getMozillaPosition(url); 94 | } 95 | } 96 | 97 | function doesFeatureHaveSpec(feature, url) { 98 | const featureSpecs = Array.isArray(feature.spec) 99 | ? feature.spec 100 | : [feature.spec]; 101 | return featureSpecs.some((spec) => spec === url); 102 | } 103 | 104 | async function findNewMozillaURLs() { 105 | // Attempt to find new vendor URLs, by matching on spec URLs. 106 | const mozData = await getMozillaData(); 107 | 108 | for (const issueId in mozData) { 109 | const issue = mozData[issueId]; 110 | if (!issue.position) { 111 | continue; 112 | } 113 | 114 | // Go over our features, and try to find a match, 115 | // by comparing spec urls. 116 | for (const featureId in features) { 117 | // Skip the features for which we already have the URL. 118 | if (positions[featureId].mozilla.url) { 119 | continue; 120 | } 121 | const matches = doesFeatureHaveSpec(features[featureId], issue.url); 122 | const issueUrl = `https://github.com/mozilla/standards-positions/issues/${issueId}`; 123 | const isWrongIssue = 124 | positions[featureId].mozilla.not && 125 | positions[featureId].mozilla.not.includes(issueUrl); 126 | if (matches && !isWrongIssue) { 127 | positions[featureId].mozilla.url = issueUrl; 128 | } 129 | } 130 | } 131 | } 132 | 133 | async function findNewWebkitURLs() { 134 | // Attempt to find new vendor URLs, by matching on spec URLs. 135 | const webkitData = await getWebkitData(); 136 | 137 | for (const issue of webkitData) { 138 | if (!issue.position) { 139 | continue; 140 | } 141 | 142 | // Go over our features, and try to find a match, 143 | // by comparing spec urls. 144 | for (const featureId in features) { 145 | // Skip the features for which we already have the URL. 146 | if (positions[featureId].webkit.url) { 147 | continue; 148 | } 149 | const matches = doesFeatureHaveSpec(features[featureId], issue.url); 150 | const isWrongIssue = 151 | positions[featureId].webkit.not && 152 | positions[featureId].webkit.not.includes(issue.id); 153 | if (matches && !isWrongIssue) { 154 | positions[featureId].webkit.url = issue.id; 155 | } 156 | } 157 | } 158 | } 159 | 160 | async function main() { 161 | // First, add any missing feature ID to the positions object. 162 | for (const id in features) { 163 | if (!positions[id]) { 164 | positions[id] = { 165 | mozilla: {}, 166 | webkit: {}, 167 | }; 168 | } 169 | } 170 | 171 | // Try to add new mozilla vendor position urls to web-features. 172 | await findNewMozillaURLs(); 173 | // Do the same for webkit. 174 | await findNewWebkitURLs(); 175 | 176 | // Finally, update the positions and concerns for the features that have vendor URLs. 177 | for (const featureId in positions) { 178 | for (const company in positions[featureId]) { 179 | if (positions[featureId][company].url) { 180 | console.log( 181 | `Updating position for ${company} in feature ${featureId}...` 182 | ); 183 | const data = await getPosition( 184 | company, 185 | positions[featureId][company].url 186 | ); 187 | positions[featureId][company].position = data.position; 188 | positions[featureId][company].concerns = data.concerns; 189 | } 190 | } 191 | } 192 | 193 | // Sort the positions by ID. 194 | const ordered = {}; 195 | Object.keys(positions) 196 | .sort() 197 | .forEach(function (id) { 198 | ordered[id] = positions[id]; 199 | }); 200 | 201 | // Store the updated positions back in the file. 202 | await fs.writeFile(OUTPUT_FILE, JSON.stringify(ordered, null, 2)); 203 | } 204 | 205 | main(); 206 | -------------------------------------------------------------------------------- /additional-data/interop.json: -------------------------------------------------------------------------------- 1 | { 2 | "2021": { 3 | "interop-2021-aspect-ratio": [ 4 | "aspect-ratio" 5 | ], 6 | "interop-2021-flexbox": [ 7 | "flexbox" 8 | ], 9 | "interop-2021-grid": [ 10 | "grid" 11 | ], 12 | "interop-2021-position-sticky": [ 13 | "sticky-positioning" 14 | ], 15 | "interop-2021-transforms": [ 16 | "transforms2d", 17 | "transforms3d" 18 | ] 19 | }, 20 | "2022": { 21 | "interop-2021-aspect-ratio": [ 22 | "aspect-ratio" 23 | ], 24 | "interop-2022-cascade": [ 25 | "cascade-layers" 26 | ], 27 | "interop-2022-color": [ 28 | "color-mix", 29 | "hsl", 30 | "hwb", 31 | "lab", 32 | "oklab", 33 | "rgb", 34 | "color-function" 35 | ], 36 | "interop-2022-contain": [ 37 | "contain", 38 | "contain-layout", 39 | "contain-paint", 40 | "contain-size" 41 | ], 42 | "interop-2022-dialog": [ 43 | "dialog" 44 | ], 45 | "interop-2021-flexbox": [ 46 | "flexbox" 47 | ], 48 | "interop-2022-forms": [], 49 | "interop-2021-grid": [ 50 | "grid" 51 | ], 52 | "interop-2022-scrolling": [ 53 | "overscroll-behavior", 54 | "scroll-snap", 55 | "scroll-behavior" 56 | ], 57 | "interop-2021-position-sticky": [ 58 | "sticky-positioning" 59 | ], 60 | "interop-2022-subgrid": [ 61 | "subgrid" 62 | ], 63 | "interop-2021-transforms": [ 64 | "transforms2d", 65 | "transforms3d" 66 | ], 67 | "interop-2022-text": [ 68 | "font-variant", 69 | "font-variant-alternates", 70 | "font-variant-caps", 71 | "font-variant-east-asian", 72 | "font-variant-ligatures", 73 | "font-variant-numeric", 74 | "font-variant-position", 75 | "ic" 76 | ], 77 | "interop-2022-viewport": [ 78 | "viewport-units" 79 | ], 80 | "interop-2022-webcompat": [] 81 | }, 82 | "2023": { 83 | "interop-2023-cssborderimage": [ 84 | "border-image" 85 | ], 86 | "interop-2023-color": [ 87 | "color-mix", 88 | "hsl", 89 | "hwb", 90 | "lab", 91 | "oklab", 92 | "rgb", 93 | "color-function", 94 | "gradient-interpolation" 95 | ], 96 | "interop-2023-container": [ 97 | "container-queries" 98 | ], 99 | "interop-2023-contain": [ 100 | "contain", 101 | "contain-layout", 102 | "contain-paint", 103 | "contain-size", 104 | "content-visibility", 105 | "contain-intrinsic-size" 106 | ], 107 | "interop-2023-mathfunctions": [ 108 | "trig-functions", 109 | "exp-functions" 110 | ], 111 | "interop-2023-pseudos": [ 112 | "dir-pseudo", 113 | "nth-child-of", 114 | "user-pseudos" 115 | ], 116 | "interop-2023-property": [ 117 | "registered-custom-properties" 118 | ], 119 | "interop-2023-flexbox": [ 120 | "flexbox" 121 | ], 122 | "interop-2023-fonts": [ 123 | "supports", 124 | "font-palette" 125 | ], 126 | "interop-2023-forms": [], 127 | "interop-2023-grid": [ 128 | "grid" 129 | ], 130 | "interop-2023-has": [ 131 | "has" 132 | ], 133 | "interop-2023-inert": [ 134 | "inert" 135 | ], 136 | "interop-2023-cssmasking": [ 137 | "clip-path" 138 | ], 139 | "interop-2023-mediaqueries": [ 140 | "media-queries", 141 | "media-query-range-syntax" 142 | ], 143 | "interop-2023-modules": [ 144 | "import-maps", 145 | "js-modules-workers" 146 | ], 147 | "interop-2023-motion": [ 148 | "motion-path" 149 | ], 150 | "interop-2023-offscreencanvas": [ 151 | "offscreen-canvas" 152 | ], 153 | "interop-2023-events": [ 154 | "mouse-events", 155 | "pointer-events-api" 156 | ], 157 | "interop-2022-scrolling": [ 158 | "overscroll-behavior", 159 | "scroll-snap", 160 | "scroll-behavior" 161 | ], 162 | "interop-2022-subgrid": [ 163 | "subgrid" 164 | ], 165 | "interop-2021-transforms": [ 166 | "transforms2d", 167 | "transforms3d" 168 | ], 169 | "interop-2023-url": [ 170 | "url" 171 | ], 172 | "interop-2023-webcodecs": [ 173 | "webcodecs" 174 | ], 175 | "interop-2023-webcompat": [], 176 | "interop-2023-webcomponents": [ 177 | "constructed-stylesheets", 178 | "form-associated-custom-elements" 179 | ] 180 | }, 181 | "2024": { 182 | "interop-2024-accessibility": [], 183 | "interop-2024-nesting": [ 184 | "nesting" 185 | ], 186 | "interop-2023-property": [ 187 | "registered-custom-properties" 188 | ], 189 | "interop-2024-dsd": [ 190 | "declarative-shadow-dom" 191 | ], 192 | "interop-2024-font-size-adjust": [ 193 | "font-size-adjust" 194 | ], 195 | "interop-2024-websockets": [], 196 | "interop-2024-indexeddb": [ 197 | "indexeddb" 198 | ], 199 | "interop-2024-layout": [ 200 | "flexbox", 201 | "grid", 202 | "subgrid" 203 | ], 204 | "interop-2023-events": [ 205 | "mouse-events", 206 | "pointer-events-api" 207 | ], 208 | "interop-2024-popover": [ 209 | "popover" 210 | ], 211 | "interop-2024-relative-color": [ 212 | "relative-color" 213 | ], 214 | "interop-2024-video-rvfc": [ 215 | "request-video-frame-callback" 216 | ], 217 | "interop-2024-scrollbar": [ 218 | "scrollbar-gutter", 219 | "scrollbar-width" 220 | ], 221 | "interop-2024-starting-style-transition-behavior": [ 222 | "starting-style", 223 | "transition-behavior" 224 | ], 225 | "interop-2024-dir": [], 226 | "interop-2024-text-wrap": [ 227 | "text-wrap", 228 | "text-wrap-balance" 229 | ], 230 | "interop-2023-url": [ 231 | "url" 232 | ] 233 | }, 234 | "2025": { 235 | "interop-2025-backdrop-filter": [ 236 | "backdrop-filter" 237 | ], 238 | "interop-2025-core-web-vitals": [ 239 | "largest-contentful-paint" 240 | ], 241 | "interop-2025-anchor-positioning": [ 242 | "anchor-positioning" 243 | ], 244 | "interop-2025-details": [ 245 | "details" 246 | ], 247 | "interop-2024-layout": [ 248 | "flexbox", 249 | "grid", 250 | "subgrid" 251 | ], 252 | "interop-2025-modules": [ 253 | "json-modules" 254 | ], 255 | "interop-2025-navigation": [ 256 | "navigation" 257 | ], 258 | "interop-2023-events": [ 259 | "pointer-events-api", 260 | "mouse-events" 261 | ], 262 | "interop-2025-remove-mutation-events": [ 263 | "mutation-events" 264 | ], 265 | "interop-2025-scope": [ 266 | "scope" 267 | ], 268 | "interop-2025-scrollend": [ 269 | "scrollend" 270 | ], 271 | "interop-2025-storageaccess": [ 272 | "storage-access" 273 | ], 274 | "interop-2025-textdecoration": [ 275 | "text-decoration" 276 | ], 277 | "interop-2025-urlpattern": [ 278 | "urlpattern" 279 | ], 280 | "interop-2025-view-transitions": [ 281 | "view-transitions", 282 | "view-transition-class" 283 | ], 284 | "interop-2025-webassembly": [ 285 | "wasm-string-builtins" 286 | ], 287 | "interop-2025-webcompat": [ 288 | "appearance", 289 | "zoom", 290 | "list-style" 291 | ], 292 | "interop-2025-webrtc": [ 293 | "webrtc-encoded-transform" 294 | ], 295 | "interop-2025-writingmodes": [ 296 | "writing-mode" 297 | ] 298 | } 299 | } -------------------------------------------------------------------------------- /update-timeline-stats.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Process baseline data from the web-features package to create 3 | * data tables for visualization purpose. 4 | * 5 | * Some notes: 6 | * - Timeline stats do not include features that have not shipped anywhere 7 | * because these aren't associated with any date in web-features (32 features 8 | * as of January 2025). 9 | * - Process drops occurrences of '≤' in dates, because it's hard to deal with 10 | * uncertainties in stats. 11 | */ 12 | 13 | import { features, browsers } from "web-features"; 14 | import fs from "node:fs/promises"; 15 | import path from "node:path"; 16 | 17 | const OUTPUT_TIMELINE = path.join(import.meta.dirname, "site", "assets", "timeline-number.json"); 18 | const OUTPUT_DURATIONS = path.join(import.meta.dirname, "site", "assets", "timeline-durations.json"); 19 | const FIRST_NEWLY_AVAILABLE_YEAR = "2015"; 20 | 21 | async function main() { 22 | // Convert features to an array and pretend all dates are exact 23 | const simplifiedFeatures = Object.entries(features) 24 | .map(([id, feature]) => Object.assign({ id }, feature)) 25 | .filter(feature => feature.kind === "feature") 26 | .map(feature => { 27 | if (feature.status.baseline_low_date && 28 | feature.status.baseline_low_date.startsWith("≤")) { 29 | feature.status.baseline_low_date = feature.status.baseline_low_date.slice(1); 30 | feature.simplified = true; 31 | } 32 | if (feature.status.baseline_high_date && 33 | feature.status.baseline_high_date.startsWith("≤")) { 34 | feature.status.baseline_high_date = feature.status.baseline_high_date.slice(1); 35 | feature.simplified = true; 36 | } 37 | for (const [browser, version] of Object.entries(feature.status.support)) { 38 | if (version.startsWith("≤")) { 39 | feature.status.support[browser] = version.slice(1); 40 | feature.simplified = true; 41 | } 42 | } 43 | return feature; 44 | }); 45 | 46 | // Compute first implementation dates and prepare full list of release dates 47 | let dates = new Set(); 48 | for (const feature of simplifiedFeatures) { 49 | feature.status.first_implementation_date = 50 | Object.entries(feature.status.support) 51 | .map(([browser, version]) => 52 | browsers[browser].releases.find(r => r.version === version)) 53 | .map(release => release.date) 54 | .sort() 55 | .reverse() 56 | .pop(); 57 | if (feature.status.baseline_high_date) { 58 | dates.add(feature.status.baseline_high_date); 59 | } 60 | if (feature.status.baseline_low_date) { 61 | dates.add(feature.status.baseline_low_date); 62 | } 63 | if (feature.status.first_implementation_date) { 64 | dates.add(feature.status.first_implementation_date); 65 | } 66 | } 67 | dates = [...dates].sort(); 68 | const years = [...new Set(dates.map(d => d.slice(0, 4)))].sort(); 69 | 70 | // Prepare timeline data 71 | let timeline = dates.map(d => Object.assign({ 72 | date: d, 73 | high: [], 74 | low: [], 75 | first: [] 76 | })); 77 | 78 | // Fill timeline data with features 79 | for (const feature of simplifiedFeatures) { 80 | let status = feature.status.baseline; 81 | if (feature.discouraged) { 82 | status = "discouraged"; 83 | } 84 | else if (feature.status.baseline === undefined) { 85 | throw new Error(`${feature.name} (id: ${feature.id}) still has an undefined baseline status!`); 86 | } 87 | else if (!feature.status.baseline) { 88 | status = "limited"; 89 | } 90 | 91 | if (feature.status.baseline_high_date) { 92 | timeline 93 | .find(t => t.date === feature.status.baseline_high_date) 94 | .high 95 | .push(feature.id); 96 | } 97 | if (feature.status.baseline_low_date) { 98 | timeline 99 | .find(t => t.date === feature.status.baseline_low_date) 100 | .low 101 | .push(feature.id); 102 | } 103 | if (feature.status.first_implementation_date) { 104 | timeline 105 | .find(t => t.date === feature.status.first_implementation_date) 106 | .first 107 | .push(feature.id); 108 | } 109 | } 110 | 111 | // Each time in the timeline contains features that shipped, became newly 112 | // or widely available, at that time. To visualize the overall growth of 113 | // the platform in terms of number of features over time, let's compile a 114 | // cumulative view. 115 | timeline = timeline.map(getCumul); 116 | 117 | // A feature that is widely available is also newly available. 118 | // A feature that is newly available is also implemented somewhere. 119 | // Let's count features only once 120 | timeline = timeline.map(t => Object.assign({ 121 | date: t.date, 122 | high: t.high, 123 | low: t.low - t.high, 124 | first: t.first - t.low 125 | })); 126 | 127 | // Export the result to a JSON file 128 | await fs.writeFile(OUTPUT_TIMELINE, JSON.stringify(timeline, null, 2), "utf8"); 129 | 130 | // Compile durations from first implementation to newly available, 131 | // and from newly available to widely available (the latter one is mostly 132 | // un-interesting for now, since it's basically always 30 months). 133 | // Note 2015 durations would not mean much because that is when Edge appears 134 | // and thus when "newly available" starts to mean something. 135 | let durations = compileDurations(simplifiedFeatures, years); 136 | durations = durations 137 | .filter(y => y.year > FIRST_NEWLY_AVAILABLE_YEAR) 138 | .filter(y => y.first2low.length > 0) 139 | .map(y => Object.assign({ 140 | year: y.year, 141 | min: y.first2low[0], 142 | q1: getQuantile(y.first2low, 0.25), 143 | median: getQuantile(y.first2low, 0.50), 144 | q3: getQuantile(y.first2low, 0.75), 145 | max: y.first2low[y.first2low.length - 1], 146 | nb: y.first2low.length 147 | })); 148 | await fs.writeFile(OUTPUT_DURATIONS, JSON.stringify(durations, null, 2), "utf8"); 149 | } 150 | 151 | main(); 152 | 153 | 154 | /********************************************************** 155 | * A few helper functions 156 | **********************************************************/ 157 | function getCumul(time, idx, list) { 158 | return { 159 | date: time.date, 160 | high: list.slice(0, idx + 1).reduce((tot, d) => tot + d.high.length, 0), 161 | low: list.slice(0, idx + 1).reduce((tot, d) => tot + d.low.length, 0), 162 | first: list.slice(0, idx + 1).reduce((tot, d) => tot + d.first.length, 0) 163 | }; 164 | } 165 | 166 | function compileDurations(features, years) { 167 | const durations = years.map(y => Object.assign({ 168 | year: y, 169 | first2low: [], 170 | low2high: [] 171 | })); 172 | for (const feature of features) { 173 | if (feature.status.baseline_low_date) { 174 | const year = feature.status.baseline_low_date.slice(0, 4); 175 | const duration = Math.floor( 176 | (new Date(feature.status.baseline_low_date) - new Date(feature.status.first_implementation_date)) / 86400000 177 | ); 178 | durations.find(y => y.year === year).first2low.push(duration); 179 | } 180 | if (feature.status.baseline_high_date) { 181 | const year = feature.status.baseline_high_date.slice(0, 4); 182 | const duration = Math.floor( 183 | (new Date(feature.status.baseline_high_date) - new Date(feature.status.baseline_low_date)) / 86400000 184 | ); 185 | durations.find(y => y.year === year).low2high.push(duration); 186 | } 187 | } 188 | 189 | for (const year of durations) { 190 | year.first2low.sort((d1, d2) => d1 - d2); 191 | year.low2high.sort((d1, d2) => d1 - d2); 192 | } 193 | return durations; 194 | } 195 | 196 | function getQuantile(arr, q) { 197 | const pos = (arr.length - 1) * q; 198 | const floor = Math.floor(pos); 199 | const rest = pos - floor; 200 | if (arr[floor + 1] !== undefined) { 201 | return arr[floor] + rest * (arr[floor + 1] - arr[floor]); 202 | } 203 | else { 204 | return arr[floor]; 205 | } 206 | }; 207 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /site/timeline.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Web features timeline 3 | layout: layout.njk 4 | --- 5 | 6 |
    7 | 8 |

    Timeline

    9 | 10 |

    This page visualizes the evolution of web platform features over time.

    11 | 12 |
    13 |

    Number of web features

    14 |

    The following chart plots the number of web features over time per Baseline type. Keep in mind that support data in web-features only covers browsers in the core browser set (Chrome, Edge, Firefox, Safari). For example, the graph contains no information about feature support in Internet Explorer (started in 1995).

    15 | 16 |

    Several dates are highlighted in the chart to ease reading:

    17 |
      18 |
    • 23 June 2003: First version of Safari and first date in the graph.
    • 19 |
    • 9 November 2004: First version of Firefox.
    • 20 |
    • 11 December 2009: First version of Chrome.
    • 21 |
    • 29 July 2015: First version of Edge (actual version number is 12). By definition, there can be no Baseline Newly Available features before that date.
    • 22 |
    • 29 January 2018: 30 months later. Also by definition, there can be no Baseline Widely Available features before that date.
    • 23 |
    • 15 January 2020: Edge switches to Chromium, creating a surge of Baseline Newly Available features.
    • 24 |
    • 15 July 2022: 30 months later, a corresponding surge of Baseline Widely Available features appears.
    • 25 |
    26 | 27 |
    28 | 29 |

    The underlying data is available in a timeline-number.json JSON file.

    30 | 31 |

    Note: Features that have not shipped anywhere are not associated with any date in web-features and do not appear in the chart. As of January 2025, ~30 features (out of 1000+ features) are in that category.

    32 | 33 |

    Replacing the values with percentages yields the following chart. It shows the evolution of the distribution of features between Baseline Widely Available, Baseline Newly Available, and features available somewhere. how interoperability progresses over time and how the web platform grows. The graph starts on 29 January 2018, when first Baseline Widely Available features appear.

    34 | 35 |
    36 |
    37 | 38 |
    39 |

    Duration from first implementation to Baseline Newly Available

    40 |

    The following chart shows the evolution of the duration needed for a feature to go from first implementation available to Baseline Newly Available, from year to year.

    41 | 42 |

    The chart is a box plot. For each year, it showcases the first quartile, the median and the last quartile (the box), along with the minimum and maximum durations (the whiskers).

    43 | 44 |
    45 | 46 |

    The underlying data is available in a timeline-durations.json JSON file.

    47 | 48 |

    Note: ~50 features in web-features have support dates that start with to indicate that support started before these dates (all of these dates are before mid-2020). This nuance is dropped to compute statistics. This makes features appear later than they should when looking at the evolution of the number of web features, and reduces the duration from first implementation to newly available. Impact should remain minimal.

    49 | 50 |

    Note: What about going from first implementation to Baseline Widely Available? Given the current definition of widely available, this would yield the same chart with additional ~910 days (30 months) to the durations.

    51 |
    52 | 53 |
    54 |

    Web platform features per year

    55 |

    The following table lists web platform features by years when they reached Baseline Newly Available status. 56 | 57 |

    58 |
    59 | 60 | 122 | 123 | 319 |
    320 | -------------------------------------------------------------------------------- /additional-data/wpt.json: -------------------------------------------------------------------------------- 1 | [ 2 | "window-management", 3 | "request-animation-frame-workers", 4 | "setinterval", 5 | "settimeout", 6 | "ua-client-hints", 7 | "online", 8 | "postmessage", 9 | "user-agent-sniffing", 10 | "shared-workers", 11 | "dedicated-workers", 12 | "xhr", 13 | "base", 14 | "webxr-device", 15 | "webvr", 16 | "webusb", 17 | "webtransport", 18 | "websockets", 19 | "webrtc-stats", 20 | "webrtc-encoded-transform", 21 | "webnn", 22 | "web-midi", 23 | "messageerror", 24 | "channel-messaging", 25 | "webhid", 26 | "webrtc", 27 | "webrtc-sctp", 28 | "webgpu", 29 | "webgl", 30 | "webauthn", 31 | "webauthn-public-key-easy", 32 | "webauthn-signals", 33 | "web-otp", 34 | "webcodecs", 35 | "share", 36 | "web-nfc", 37 | "web-locks", 38 | "web-animations", 39 | "virtual-keyboard", 40 | "visual-viewport", 41 | "viewport-segments", 42 | "viewport", 43 | "request-video-frame-callback", 44 | "vibration", 45 | "urlpattern", 46 | "url", 47 | "url-canparse", 48 | "upgrade-insecure-requests", 49 | "touch-events", 50 | "trusted-types", 51 | "streams", 52 | "storage-access", 53 | "speech-recognition", 54 | "speech-synthesis", 55 | "speculation-rules", 56 | "barcode", 57 | "shared-storage", 58 | "shared-storage-locks", 59 | "server-timing", 60 | "serial", 61 | "shadow-dom", 62 | "slot-assign", 63 | "document-caretpositionfrompoint", 64 | "is-secure-context", 65 | "secure-payment-confirmation", 66 | "scroll-driven-animations", 67 | "scroll-to-text-fragment", 68 | "screen-wake-lock", 69 | "selection-api", 70 | "screen-orientation", 71 | "screen-orientation-lock", 72 | "savedata", 73 | "screen-capture", 74 | "scheduler", 75 | "sanitizer", 76 | "resize-observer", 77 | "requestidlecallback", 78 | "reporting", 79 | "resource-timing", 80 | "remote-playback", 81 | "referrer-policy", 82 | "private-click-measurement", 83 | "presentation-api", 84 | "modulepreload", 85 | "preloading-responsive-images", 86 | "pointer-lock", 87 | "picture-in-picture", 88 | "permissions", 89 | "periodic-background-sync", 90 | "pointer-events-api", 91 | "payment-request", 92 | "payment-handler", 93 | "paint-timing", 94 | "page-visibility-state", 95 | "page-lifecycle", 96 | "device-orientation-events", 97 | "orientation-sensor", 98 | "notifications", 99 | "network-information", 100 | "navigation", 101 | "navigation-timing", 102 | "mixed-content", 103 | "media-session", 104 | "mediastream-recording", 105 | "element-capture", 106 | "capture-stream-audio-video", 107 | "capture-stream-canvas", 108 | "media-playback-quality", 109 | "media-capabilities", 110 | "media-source", 111 | "mathml", 112 | "measure-memory", 113 | "magnetometer", 114 | "longtasks", 115 | "long-animation-frames", 116 | "keyboard-map", 117 | "layout-instability", 118 | "largest-contentful-paint", 119 | "keyboard-lock", 120 | "profiler", 121 | "jpegxl", 122 | "is-input-pending", 123 | "intersection-observer", 124 | "clipboard-events", 125 | "contenteditable", 126 | "inert", 127 | "import-maps", 128 | "imagebitmaprenderingcontext", 129 | "idle-detection", 130 | "html-media-capture", 131 | "figure", 132 | "hsts", 133 | "gyroscope", 134 | "gpc", 135 | "geolocation", 136 | "virtual-sensor", 137 | "gamepad", 138 | "gamepad-haptics", 139 | "fullscreen", 140 | "local-fonts", 141 | "forced-colors", 142 | "caret-color", 143 | "file-system-access", 144 | "origin-private-file-system", 145 | "fedcm", 146 | "eyedropper", 147 | "server-sent-events", 148 | "fencedframe", 149 | "event-timing", 150 | "text-encoding", 151 | "encrypted-media-extensions", 152 | "element-timing", 153 | "xpath", 154 | "domparser", 155 | "xml-serializer", 156 | "document-picture-in-picture", 157 | "device-posture", 158 | "device-memory", 159 | "srcset", 160 | "autonomous-custom-elements", 161 | "customized-built-in-elements", 162 | "credential-management", 163 | "federated-credentials", 164 | "cors", 165 | "contenteditable-plaintextonly", 166 | "cookie-store", 167 | "content-index", 168 | "csp", 169 | "contact-picker", 170 | "console", 171 | "compute-pressure", 172 | "compression-streams", 173 | "text-stroke-fill", 174 | "closewatcher", 175 | "async-clipboard", 176 | "clipboard-custom-format", 177 | "clipboard-svg", 178 | "clipboard-unsanitized-formats", 179 | "clipboardchange", 180 | "clear-site-data", 181 | "web-bluetooth", 182 | "beacons", 183 | "battery", 184 | "badging", 185 | "background-sync", 186 | "avif", 187 | "background-fetch", 188 | "attribution-reporting", 189 | "manifest", 190 | "ambient-light", 191 | "accelerometer", 192 | "web-cryptography", 193 | "file", 194 | "indexeddb", 195 | "js-modules-workers", 196 | "js-modules-shared-workers", 197 | "plane-detection", 198 | "webxr-lighting-estimation", 199 | "webxr-layers", 200 | "webxr-hit-test", 201 | "webxr-hand-input", 202 | "webxr-gamepads", 203 | "webxr-dom-overlays", 204 | "webxr-depth-sensing", 205 | "webxr-camera", 206 | "webxr-ar", 207 | "webxr-anchors", 208 | "broadcast-channel", 209 | "wasm", 210 | "wasm-reference-types", 211 | "wasm-tail-call-optimization", 212 | "wasm-typed-fun-refs", 213 | "accesskey", 214 | "svg-filters", 215 | "fetch-priority", 216 | "smil-svg-animations", 217 | "transformstream-transformer-cancel", 218 | "transferable-streams", 219 | "readable-byte-streams", 220 | "async-iterable-streams", 221 | "readablestream-from", 222 | "storage-buckets", 223 | "js-modules-service-workers", 224 | "declarative-shadow-dom", 225 | "gethtml", 226 | "bfcache-blocking-reasons", 227 | "autofocus", 228 | "intersection-observer-v2", 229 | "user-activation", 230 | "aria-attribute-reflection", 231 | "backdrop", 232 | "fetch-metadata", 233 | "local-network-access", 234 | "execcommand", 235 | "edit-context", 236 | "xslt", 237 | "observable", 238 | "dataset", 239 | "aborting", 240 | "abortsignal-any", 241 | "change-event", 242 | "mutationobserver", 243 | "state", 244 | "scoped-custom-element-registries", 245 | "form-associated-custom-elements", 246 | "dir-pseudo", 247 | "empty", 248 | "focus-visible", 249 | "has", 250 | "read-write-pseudos", 251 | "modal", 252 | "not", 253 | "nth-child-of", 254 | "user-pseudos", 255 | "user-action-pseudos", 256 | "open-pseudo", 257 | "prefers-color-scheme", 258 | "prefers-contrast", 259 | "prefers-reduced-transparency", 260 | "overflow", 261 | "dynamic-range", 262 | "display-mode", 263 | "update", 264 | "scripting", 265 | "color-gamut", 266 | "motion-path", 267 | "paint-order", 268 | "constructed-stylesheets", 269 | "all", 270 | "alternative-style-sheets", 271 | "font-face", 272 | "font-variant", 273 | "will-change", 274 | "backdrop-filter", 275 | "view-transitions", 276 | "view-transition-class", 277 | "active-view-transition", 278 | "custom-properties", 279 | "accent-color", 280 | "appearance", 281 | "outline", 282 | "resize", 283 | "user-select", 284 | "css-typed-om", 285 | "numeric-factory-functions", 286 | "abs-sign", 287 | "attr", 288 | "cap", 289 | "rcap", 290 | "exp-functions", 291 | "ic", 292 | "ric", 293 | "if", 294 | "lh", 295 | "rlh", 296 | "ch", 297 | "rch", 298 | "ex", 299 | "rex", 300 | "round-mod-rem", 301 | "trig-functions", 302 | "calc", 303 | "calc-constants", 304 | "viewport-unit-variants", 305 | "min-max-clamp", 306 | "viewport-units", 307 | "border-radius", 308 | "transitions", 309 | "starting-style", 310 | "transition-behavior", 311 | "steps-easing", 312 | "cubic-bezier-easing", 313 | "clip", 314 | "text-emphasis", 315 | "text-underline-offset", 316 | "text-combine-upright", 317 | "nesting", 318 | "style-attr", 319 | "individual-transforms", 320 | "transforms3d", 321 | "check-visibility", 322 | "element-from-point", 323 | "screen", 324 | "scroll-into-view", 325 | "shadow-parts", 326 | "scrollbar-color", 327 | "scrollbar-width", 328 | "scroll-snap", 329 | "overflow-anchor", 330 | "rhythmic-sizing", 331 | "fit-content", 332 | "has-slotted", 333 | "host-context", 334 | "registered-custom-properties", 335 | "ruby", 336 | "ruby-align", 337 | "ruby-position", 338 | "ruby-overhang", 339 | "absolute-positioning", 340 | "fixed-positioning", 341 | "z-index", 342 | "target-text", 343 | "spelling-grammar-error", 344 | "marker", 345 | "file-selector-button", 346 | "first-letter", 347 | "first-line", 348 | "before-after", 349 | "page-orientation", 350 | "page-selectors", 351 | "function", 352 | "mixin", 353 | "masks", 354 | "logical-properties", 355 | "counter-set", 356 | "scroll-markers", 357 | "scrollbar-buttons", 358 | "scrollbar-gutter", 359 | "overflow-overlay", 360 | "overflow-shorthand", 361 | "highlight", 362 | "grid", 363 | "font-metric-overrides", 364 | "conic-gradients", 365 | "object-view-box", 366 | "crisp-edges", 367 | "safe-area-inset", 368 | "linear-easing", 369 | "attr-contents", 370 | "quotes", 371 | "supports", 372 | "supports-compat", 373 | "font-palette", 374 | "font-size-adjust", 375 | "font-synthesis", 376 | "font-synthesis-position", 377 | "font-synthesis-small-caps", 378 | "font-synthesis-style", 379 | "font-synthesis-weight", 380 | "font-variant-alternates", 381 | "font-variant-caps", 382 | "font-variant-east-asian", 383 | "font-variant-emoji", 384 | "font-variant-ligatures", 385 | "font-variant-numeric", 386 | "font-variant-position", 387 | "font-language-override", 388 | "font-family-ui", 389 | "font-feature-settings", 390 | "font-weight", 391 | "contain-inline-size", 392 | "cascade-layers", 393 | "scope", 394 | "revert-value", 395 | "borders", 396 | "color-function", 397 | "color-mix", 398 | "lab", 399 | "light-dark", 400 | "oklab", 401 | "hwb", 402 | "relative-color", 403 | "rgb", 404 | "system-color", 405 | "contrast-color", 406 | "hsl", 407 | "animation-composition", 408 | "display-animation", 409 | "animations-css", 410 | "box-decoration-break", 411 | "page-break-aliases", 412 | "widows-orphans", 413 | "background-image", 414 | "background", 415 | "background-clip", 416 | "border-image", 417 | "background-attachment", 418 | "background-color", 419 | "background-origin", 420 | "background-position", 421 | "background-repeat", 422 | "background-size", 423 | "flexbox", 424 | "anchor-positioning", 425 | "app-shortcuts", 426 | "app-protocol-handlers", 427 | "app-file-handlers", 428 | "window-controls-overlay", 429 | "summarizer", 430 | "alt-text-generated-content", 431 | "webvtt", 432 | "webvtt-regions", 433 | "webvtt-cue-settings", 434 | "webvtt-cue-alignment", 435 | "webdriver-bidi", 436 | "web-audio", 437 | "offline-audio-context", 438 | "audio-worklet", 439 | "wasm-exception-handling", 440 | "wasm-threads", 441 | "wasm-string-builtins", 442 | "wasm-mutable-globals", 443 | "wasm-multi-value", 444 | "wasm-simd-relaxed", 445 | "wasm-simd", 446 | "wasm-multi-memory", 447 | "wasm-memory64", 448 | "wasm-garbage-collection", 449 | "wasm-exnref-exceptions", 450 | "wasm-bulk-memory", 451 | "wheel-events", 452 | "svg-discouraged", 453 | "dominant-baseline", 454 | "target", 455 | "download", 456 | "context-fill-stroke", 457 | "alerts", 458 | "structured-clone", 459 | "base64encodedecode", 460 | "table", 461 | "popover", 462 | "fieldset", 463 | "field-sizing", 464 | "textarea", 465 | "details", 466 | "details-content", 467 | "tabindex", 468 | "input-file", 469 | "hidden-until-found", 470 | "draganddrop", 471 | "blocking-render", 472 | "bdi", 473 | "offscreen-canvas", 474 | "window", 475 | "barprop", 476 | "zstd", 477 | "fetch-request-streams", 478 | "early-data", 479 | "abortable-fetch", 480 | "scrollend", 481 | "move-before", 482 | "media-pseudos", 483 | "zoom", 484 | "cross-document-view-transitions", 485 | "sibling-count", 486 | "interpolate-size", 487 | "calc-size", 488 | "vertical-form-controls", 489 | "word-spacing", 490 | "word-break-break-word", 491 | "word-break", 492 | "text-wrap-balance", 493 | "text-wrap-nowrap", 494 | "white-space-collapse", 495 | "text-spacing-trim", 496 | "text-indent", 497 | "text-indent-each-line", 498 | "text-indent-hanging", 499 | "tab-size", 500 | "text-align", 501 | "text-align-last", 502 | "line-break", 503 | "text-wrap-pretty", 504 | "text-wrap-style", 505 | "text-wrap", 506 | "text-wrap-mode", 507 | "hyphens", 508 | "hyphenate-character", 509 | "hyphenate-limit-chars", 510 | "transform-box", 511 | "speak-as", 512 | "scroll-snap-events", 513 | "scroll-initial-target", 514 | "stretch", 515 | "contain-intrinsic-size", 516 | "aspect-ratio", 517 | "color-contrast", 518 | "sticky-positioning", 519 | "overlay", 520 | "clip-path-boxes", 521 | "clip-path", 522 | "clip-path-animatable", 523 | "alignment-baseline", 524 | "baseline-shift", 525 | "baseline-source", 526 | "text-box", 527 | "initial-letter", 528 | "line-clamp", 529 | "subgrid", 530 | "masonry", 531 | "grid-animation", 532 | "image-orientation", 533 | "gradient-interpolation", 534 | "smooth", 535 | "image-set", 536 | "counter-style", 537 | "two-value-display", 538 | "print-color-adjust", 539 | "container-queries", 540 | "container-style-queries", 541 | "font-optical-sizing", 542 | "font-palette-animation", 543 | "content-visibility", 544 | "float-clear", 545 | "margin-trim", 546 | "align-content-block", 547 | "background-blend-mode", 548 | "isolation", 549 | "background-clip-text", 550 | "background-clip-border-area", 551 | "em-unit", 552 | "charset", 553 | "reading-flow", 554 | "pdf-viewer", 555 | "registerprotocolhandler", 556 | "parse-html-unsafe", 557 | "template", 558 | "style", 559 | "wbr", 560 | "time", 561 | "br", 562 | "bdo", 563 | "b", 564 | "autofill", 565 | "default", 566 | "indeterminate", 567 | "a", 568 | "ping", 569 | "details-name", 570 | "pre", 571 | "p", 572 | "dialog", 573 | "dialog-closedby", 574 | "requestclose", 575 | "hr", 576 | "div", 577 | "progress", 578 | "constraint-validation", 579 | "output", 580 | "show-picker-select", 581 | "meter", 582 | "label", 583 | "datalist", 584 | "show-picker-input", 585 | "input-date-time", 586 | "dirname", 587 | "embed", 588 | "loading-lazy", 589 | "audio", 590 | "ins", 591 | "del", 592 | "title", 593 | "preserves-pitch", 594 | "table-discouraged", 595 | "search", 596 | "spellcheck", 597 | "writingsuggestions", 598 | "autocorrect", 599 | "autocapitalize", 600 | "translate", 601 | "title-attr", 602 | "canvas-2d-color-management", 603 | "canvas-reset", 604 | "canvas-roundrect", 605 | "canvas-createconicgradient", 606 | "history", 607 | "beforeunload", 608 | "speak", 609 | "word-break-auto-phrase", 610 | "color-scheme", 611 | "container-scroll-state-queries", 612 | "css-modules", 613 | "source", 614 | "canvas-2d-alpha", 615 | "text-tracks", 616 | "time-relative-selectors" 617 | ] -------------------------------------------------------------------------------- /site/assets/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --baseline-low-bg: #e8f0fe; 3 | --baseline-low-border: #d2e3fc; 4 | --baseline-high-bg: #e6f4ea; 5 | --baseline-high-border: #c9ecd3; 6 | --baseline-limited-bg: #f1f3f4; 7 | --baseline-limited-border: #e3e6e8; 8 | --browser-supported-bg: #ceead6; 9 | --browser-unsupported-bg: #f5d6d6; 10 | --text: black; 11 | --background: white; 12 | --background-alt: #f5f5f5; 13 | --sub-text: #666; 14 | --margin: 1.5rem; 15 | } 16 | 17 | @media (min-width: 600px) { 18 | :root { 19 | --margin: 2.3rem; 20 | } 21 | } 22 | 23 | html { 24 | font-size: 0.9rem; 25 | font-family: Verdana, sans-serif; 26 | color: var(--text); 27 | background: var(--background); 28 | line-height: 1.5; 29 | } 30 | 31 | body { 32 | margin: 0; 33 | } 34 | 35 | header { 36 | padding: var(--margin); 37 | text-align: center; 38 | background: var(--baseline-high-bg); 39 | border-block-end: 1px solid var(--baseline-high-border); 40 | } 41 | 42 | header>a { 43 | display: inline-block; 44 | font-size: 1.7rem; 45 | margin-block-end: var(--margin); 46 | 47 | text-decoration: none; 48 | color: var(--text); 49 | padding-inline-start: 3rem; 50 | background-image: url(logo.png); 51 | background-repeat: no-repeat; 52 | background-size: 2rem; 53 | background-position: 0 center; 54 | } 55 | 56 | @media (max-width: 600px) { 57 | header>a { 58 | padding-inline-start: 0; 59 | padding-block-start: 2.5rem; 60 | background-position: center 0; 61 | font-size: 1.5rem; 62 | } 63 | } 64 | 65 | h1 { 66 | margin: var(--margin) 0; 67 | font-size: 1.5rem; 68 | font-weight: normal; 69 | } 70 | 71 | h2 { 72 | font-size: 1.3rem; 73 | font-weight: normal; 74 | } 75 | 76 | h1 code { 77 | font-size: inherit; 78 | } 79 | 80 | h1 img { 81 | height: 2rem; 82 | } 83 | 84 | .intro { 85 | margin: var(--margin) 0; 86 | } 87 | 88 | .intro dl { 89 | display: grid; 90 | grid-template-columns: max-content auto; 91 | gap: 0.5rem; 92 | } 93 | 94 | .intro dl dt { 95 | font-weight: bold; 96 | } 97 | 98 | .intro dl dd { 99 | margin: 0; 100 | } 101 | 102 | ul, 103 | li { 104 | margin: 0; 105 | padding: 0; 106 | list-style: none; 107 | } 108 | 109 | h3 .subtext { 110 | font-size: small; 111 | font-weight: normal; 112 | } 113 | 114 | code { 115 | background: #0001; 116 | padding: 0.125rem; 117 | font-size: 0.9rem; 118 | border-radius: 0.125rem; 119 | } 120 | 121 | a:hover { 122 | text-decoration: none; 123 | } 124 | 125 | /* Feature box */ 126 | 127 | .feature { 128 | padding: var(--margin); 129 | border-radius: 0.5rem; 130 | margin-block-start: var(--margin); 131 | } 132 | 133 | .feature.mini { 134 | padding: calc(var(--margin) / 2); 135 | margin-block-start: calc(var(--margin) / 2); 136 | } 137 | 138 | .baseline-false { 139 | --bg: var(--baseline-limited-bg); 140 | --border: var(--baseline-limited-border); 141 | } 142 | 143 | .baseline-low { 144 | --bg: var(--baseline-low-bg); 145 | --border: var(--baseline-low-border); 146 | } 147 | 148 | .baseline-high { 149 | --bg: var(--baseline-high-bg); 150 | --border: var(--baseline-high-border); 151 | } 152 | 153 | .feature.baseline-false { 154 | background: linear-gradient(to bottom, 155 | var(--baseline-limited-bg), 156 | var(--background)); 157 | } 158 | 159 | .feature.baseline-low { 160 | background: linear-gradient(to bottom, 161 | var(--baseline-low-bg), 162 | var(--background)); 163 | } 164 | 165 | .feature.baseline-high { 166 | background: linear-gradient(to bottom, 167 | var(--baseline-high-bg), 168 | var(--background)); 169 | } 170 | 171 | .feature.discouraged { 172 | background: linear-gradient(to bottom, 173 | var(--browser-unsupported-bg), 174 | var(--background)); 175 | } 176 | 177 | .feature .header { 178 | display: flex; 179 | gap: 0.5rem; 180 | flex-wrap: wrap; 181 | align-items: center; 182 | justify-content: space-between; 183 | } 184 | 185 | .feature h2, 186 | .feature h1 { 187 | margin: 0; 188 | } 189 | 190 | .feature>p { 191 | margin: 0; 192 | line-height: 1.6; 193 | } 194 | 195 | .feature.mini>p { 196 | margin: 0; 197 | } 198 | 199 | .feature.short>p { 200 | margin: 1rem 0; 201 | } 202 | 203 | .feature.short h2 a { 204 | text-decoration: none; 205 | color: var(--text); 206 | } 207 | 208 | .feature.short h2 a code { 209 | font-size: inherit; 210 | } 211 | 212 | .availability { 213 | background: var(--bg); 214 | border: 2px solid var(--border); 215 | padding: 0.5rem; 216 | border-radius: 0.25rem; 217 | padding-inline-start: 2.5rem; 218 | background-repeat: no-repeat; 219 | background-position: 0.5rem center; 220 | background-size: 1.5rem; 221 | margin-left: auto; 222 | } 223 | 224 | .baseline-false .availability { 225 | background-image: url(https://web-platform-dx.github.io/web-features/assets/img/baseline-limited-icon.svg); 226 | } 227 | 228 | .baseline-low .availability { 229 | background-image: url(https://web-platform-dx.github.io/web-features/assets/img/baseline-newly-icon.svg); 230 | } 231 | 232 | .baseline-high .availability { 233 | background-image: url(https://web-platform-dx.github.io/web-features/assets/img/baseline-widely-icon.svg); 234 | } 235 | 236 | .discouraged .availability { 237 | background-image: none; 238 | padding-inline-start: 0.25rem; 239 | } 240 | 241 | .tags { 242 | font-size: 0.6rem; 243 | text-transform: uppercase; 244 | font-weight: bold; 245 | } 246 | 247 | .tags .tag { 248 | padding: 0.125rem 0.25rem; 249 | border-radius: 0.25rem; 250 | background: var(--background); 251 | border: 1px solid var(--text); 252 | } 253 | 254 | .compat { 255 | display: flex; 256 | flex-wrap: wrap; 257 | gap: 0.25rem; 258 | } 259 | 260 | .compat-full { 261 | display: block; 262 | } 263 | 264 | .compat .browser { 265 | --bg: var(--browser-unsupported-bg); 266 | padding: 1.25rem 0.55rem 0.25rem 0.5rem; 267 | border-radius: 0.25rem; 268 | border: 2px solid color-mix(in srgb, var(--bg) 90%, black); 269 | background-color: var(--bg); 270 | background-position: center 0.25rem; 271 | background-repeat: no-repeat; 272 | background-size: 1rem; 273 | display: flex; 274 | flex-direction: column; 275 | align-items: center; 276 | min-width: 4rem; 277 | } 278 | 279 | .compat-full .browser { 280 | padding: 0.5rem 0.5rem 0.5rem 3rem; 281 | border: none; 282 | background-position: -0.5rem center; 283 | background-size: 3rem; 284 | background-blend-mode: overlay; 285 | margin: 0.125rem 0; 286 | display: block; 287 | } 288 | 289 | .compat .browser.supported { 290 | --bg: var(--browser-supported-bg); 291 | } 292 | 293 | .compat .browser-chrome, 294 | .compat .browser-chrome_android { 295 | background-image: url(./chrome.svg); 296 | } 297 | 298 | .compat .browser-firefox, 299 | .compat .browser-firefox_android { 300 | background-image: url(./firefox.svg); 301 | } 302 | 303 | .compat .browser-edge { 304 | background-image: url(./edge.svg); 305 | } 306 | 307 | .compat .browser-safari, 308 | .compat .browser-safari_ios { 309 | background-image: url(./safari.svg); 310 | } 311 | 312 | .compat .browser .name { 313 | font-weight: bold; 314 | } 315 | 316 | .compat .browser .date, 317 | .compat .browser .bug, 318 | .compat .browser .position { 319 | font-size: 0.75rem; 320 | } 321 | 322 | .compat .position { 323 | color: black; 324 | font-weight: bold; 325 | padding: 0.25rem; 326 | border-radius: 0.25rem; 327 | margin: 0.25rem 0 0 0; 328 | box-decoration-break: clone; 329 | -webkit-box-decoration-break: clone; 330 | line-height: 2; 331 | } 332 | 333 | .compat .position-oppose, 334 | .compat .position-negative { 335 | background: color-mix(in srgb, var(--browser-unsupported-bg) 90%, black); 336 | } 337 | 338 | .compat .position-support, 339 | .compat .position-positive { 340 | background: color-mix(in srgb, var(--browser-supported-bg) 90%, black); 341 | } 342 | 343 | .feature .resources, 344 | .feature .resources li { 345 | list-style: disc; 346 | padding-inline-start: 0.5rem; 347 | margin-inline-start: 0.5rem; 348 | } 349 | 350 | /* Feature page */ 351 | 352 | .feature-page { 353 | padding: 0 var(--margin); 354 | } 355 | 356 | .feature-page .feature { 357 | background: none; 358 | padding: 0; 359 | column-width: 25rem; 360 | gap: calc(var(--margin) / 2); 361 | margin-block-start: 0; 362 | } 363 | 364 | .feature-page h3 { 365 | margin-block-start: 0; 366 | } 367 | 368 | .feature-page .header, 369 | .feature-page .edit-links, 370 | .feature-page .tags, 371 | .feature-page .description, 372 | .feature-page .discouraged-info { 373 | column-span: all; 374 | margin: var(--margin) 0; 375 | } 376 | 377 | .feature-page .discouraged-info { 378 | background: var(--browser-unsupported-bg); 379 | padding: calc(var(--margin) / 2); 380 | border-radius: 0.25rem; 381 | } 382 | 383 | .feature-page .discouraged-info p { 384 | margin: 0; 385 | } 386 | 387 | .feature-page .feature-box { 388 | background: var(--background-alt); 389 | padding: calc(var(--margin) / 2); 390 | border-radius: 0.5rem; 391 | margin-block-end: calc(var(--margin) / 2); 392 | break-inside: avoid-column; 393 | } 394 | 395 | .feature-page .feature-box.baseline { 396 | background: transparent; 397 | border: 2px solid var(--border); 398 | } 399 | 400 | /* Nav */ 401 | 402 | nav { 403 | display: flex; 404 | flex-wrap: wrap; 405 | gap: 0.5rem; 406 | justify-content: center; 407 | } 408 | 409 | nav ul { 410 | display: flex; 411 | flex-wrap: wrap; 412 | justify-content: center; 413 | gap: 0.5rem; 414 | } 415 | 416 | nav a { 417 | display: block; 418 | padding: 0.5rem; 419 | color: var(--text); 420 | } 421 | 422 | nav li { 423 | border-radius: 0.25rem; 424 | background-image: linear-gradient(to top right, 425 | var(--baseline-high-border), 426 | var(--baseline-low-border), 427 | var(--baseline-limited-border)); 428 | filter: saturate(2); 429 | box-shadow: 0 0 0.5rem 0 #fff; 430 | } 431 | 432 | /* Search box */ 433 | 434 | header search form { 435 | height: 100%; 436 | display: flex; 437 | } 438 | 439 | header search input { 440 | font-family: inherit; 441 | width: 9rem; 442 | /* Prevent iOS from zooming on the field when focus */ 443 | font-size: 16px; 444 | padding: 0.25rem; 445 | margin: 0; 446 | height: 100%; 447 | box-sizing: border-box; 448 | border-radius: 0.25rem; 449 | border: 2px solid var(--browser-supported-bg); 450 | box-shadow: 0 0 0.5rem 0 white; 451 | } 452 | 453 | header output ul { 454 | text-align: start; 455 | display: flex; 456 | flex-direction: column; 457 | gap: 1rem; 458 | margin-block-start: var(--margin); 459 | } 460 | 461 | header output a { 462 | display: block; 463 | background: var(--background-alt); 464 | padding: calc(var(--margin) / 2); 465 | border-radius: 0.5rem; 466 | color: var(--text); 467 | text-decoration: none; 468 | } 469 | 470 | header output a:hover { 471 | text-decoration: underline; 472 | } 473 | 474 | header output h3 { 475 | margin: 0; 476 | font-size: 1rem; 477 | } 478 | 479 | header output p { 480 | margin-block-end: 0; 481 | } 482 | 483 | /* Main */ 484 | 485 | main { 486 | padding: 0 var(--margin); 487 | } 488 | 489 | /* Home page */ 490 | 491 | .home>p { 492 | margin: var(--margin) 0; 493 | } 494 | 495 | /* List of all release notes */ 496 | 497 | .monthly-update-list { 498 | display: grid; 499 | gap: var(--margin); 500 | grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); 501 | } 502 | 503 | .monthly-update-list .month-entry { 504 | padding: calc(var(--margin) / 2); 505 | background: var(--background-alt); 506 | border-radius: 0.5rem; 507 | } 508 | 509 | .monthly-update-list .month-entry h2 { 510 | margin: 0 0 1rem 0; 511 | font-size: 1.3rem; 512 | } 513 | 514 | .monthly-update-list .month-entry li { 515 | padding: 0.125rem 0; 516 | } 517 | 518 | /* Individual month release note */ 519 | 520 | .release-notes:not(.home) { 521 | display: grid; 522 | column-gap: var(--margin); 523 | grid-template-columns: 1fr 13rem; 524 | } 525 | 526 | .release-notes h1 { 527 | grid-column: span 2; 528 | } 529 | 530 | .release-notes section { 531 | padding: var(--margin); 532 | border-radius: 0.5rem; 533 | margin-block-end: var(--margin); 534 | } 535 | 536 | .release-notes .baseline-low { 537 | background: linear-gradient(to bottom, 538 | var(--baseline-low-bg), 539 | var(--background)); 540 | } 541 | 542 | .release-notes .baseline-high { 543 | background: linear-gradient(to bottom, 544 | var(--baseline-high-bg), 545 | var(--background)); 546 | } 547 | 548 | .release-notes .baseline-false { 549 | background: linear-gradient(to bottom, 550 | var(--baseline-limited-bg), 551 | var(--background)); 552 | } 553 | 554 | .release-notes h2 { 555 | margin-block-start: 0; 556 | font-size: 1.2rem; 557 | } 558 | 559 | .release-notes section h2 { 560 | padding-inline-start: 3rem; 561 | background-repeat: no-repeat; 562 | background-size: 1.7rem; 563 | background-position: 0 center; 564 | } 565 | 566 | .release-notes .baseline-low h2 { 567 | background-image: url(https://web-platform-dx.github.io/web-features/assets/img/baseline-newly-icon.svg); 568 | } 569 | 570 | .release-notes .baseline-high h2 { 571 | background-image: url(https://web-platform-dx.github.io/web-features/assets/img/baseline-widely-icon.svg); 572 | } 573 | 574 | .release-notes .safari h2, 575 | .release-notes .safari_ios h2 { 576 | background-image: url(./safari.svg); 577 | } 578 | 579 | .release-notes .firefox h2, 580 | .release-notes .firefox_android h2 { 581 | background-image: url(./firefox.svg); 582 | } 583 | 584 | .release-notes .chrome h2, 585 | .release-notes .chrome_android h2 { 586 | background-image: url(./chrome.svg); 587 | } 588 | 589 | .release-notes .edge h2 { 590 | background-image: url(./edge.svg); 591 | } 592 | 593 | .release-notes section ul { 594 | column-gap: calc(var(--margin) / 2); 595 | column-width: 20rem; 596 | } 597 | 598 | .release-notes section li { 599 | break-inside: avoid; 600 | margin: 0 0 var(--margin) 1rem; 601 | list-style: disc; 602 | } 603 | 604 | .release-notes aside { 605 | background: var(--background-alt); 606 | padding: calc(var(--margin) / 2); 607 | border-radius: 0.5rem; 608 | place-self: start stretch; 609 | } 610 | 611 | .release-notes aside ul { 612 | line-height: 2; 613 | } 614 | 615 | @media (max-width: 750px) { 616 | .release-notes:not(.home) { 617 | display: block; 618 | } 619 | 620 | .release-notes:not(.home) aside { 621 | padding: var(--margin); 622 | } 623 | } 624 | 625 | /* Feature list pages */ 626 | 627 | .explore-features .views { 628 | display: flex; 629 | flex-wrap: wrap; 630 | gap: 1.2rem 0.5rem; 631 | margin: 0 calc(var(--margin) * -1) var(--margin); 632 | padding: var(--margin); 633 | background: var(--baseline-limited-bg); 634 | border-block-end: 1px solid var(--baseline-limited-border); 635 | } 636 | 637 | .explore-features .views a { 638 | padding: 0.5rem; 639 | border-radius: 0.25rem; 640 | border: 1px solid #0002; 641 | } 642 | 643 | /* Groups */ 644 | 645 | .groups li { 646 | padding: 0.25rem; 647 | } 648 | 649 | .groups li li { 650 | margin-inline-start: 2rem; 651 | } 652 | 653 | .groups summary { 654 | cursor: pointer; 655 | } 656 | 657 | .groups details[open] { 658 | background-image: linear-gradient(to right, black 1px, transparent 0); 659 | background-position: 0.3rem 1rem; 660 | background-size: 100% calc(100% - 1rem); 661 | background-repeat: no-repeat; 662 | } 663 | 664 | /* Timeline concept */ 665 | 666 | #timeline { 667 | overflow-x: auto; 668 | overflow-y: auto; 669 | max-width: fit-content; 670 | display: grid; 671 | grid-auto-columns: 7rem; 672 | font-size: 0.6rem; 673 | gap: 1px; 674 | background: var(--text); 675 | } 676 | 677 | #timeline .timeline-year { 678 | grid-row: 1; 679 | display: grid; 680 | grid-auto-rows: 1rem; 681 | width: 6.5rem; 682 | background: var(--background); 683 | padding: 0 0.25rem; 684 | } 685 | 686 | #timeline .timeline-year h2 { 687 | font-size: inherit; 688 | margin: 0; 689 | } 690 | 691 | #timeline .timeline-feature { 692 | overflow: hidden; 693 | text-overflow: ellipsis; 694 | white-space: nowrap; 695 | } 696 | 697 | /* Browse page */ 698 | 699 | .feature-browser summary { 700 | cursor: pointer; 701 | background: white; 702 | position: relative; 703 | inset-inline: -0.2rem; 704 | } 705 | 706 | .feature-browser summary:hover { 707 | text-decoration: underline; 708 | } 709 | 710 | .feature-browser ul ul { 711 | padding-inline-start: calc(var(--margin) / 2); 712 | } 713 | 714 | .feature-browser .features li { 715 | padding: .5rem; 716 | background: #eee; 717 | margin: .5rem 0; 718 | border-radius: .5rem; 719 | } 720 | 721 | .feature-browser .name { 722 | display: block; 723 | } 724 | 725 | .feature-browser .group { 726 | background-image: linear-gradient(to right, black 1px, transparent 0); 727 | background-position: 0 0; 728 | } 729 | 730 | .edit-links { 731 | margin-block-start: var(--margin); 732 | display: block; 733 | font-size: 0.8rem; 734 | text-align: center; 735 | } 736 | 737 | /* Tables */ 738 | 739 | table { 740 | border-collapse: collapse; 741 | border: 1px solid var(--text); 742 | font-size: 0.8rem; 743 | } 744 | 745 | th, 746 | td { 747 | border: 1px solid var(--text); 748 | padding: calc(var(--margin) / 4); 749 | } 750 | 751 | table ul, 752 | table li { 753 | display: inline; 754 | margin: 0; 755 | padding: 0; 756 | list-style: none; 757 | } 758 | 759 | /* ID copy button */ 760 | 761 | .feature-copyable-id * { 762 | font-family: monospace; 763 | font-size: .8rem; 764 | margin: 0; 765 | padding: .25rem; 766 | border: 1px solid #ccc; 767 | color: var(--sub-text); 768 | } 769 | 770 | .feature-copyable-id input { 771 | border-inline-end: none; 772 | border-radius: 0.25rem 0 0 0.25rem; 773 | width: clamp(3ch, calc(attr(data-len ch, 10ch) + 0.5rem), 40ch); 774 | text-overflow: ellipsis; 775 | } 776 | 777 | .feature-copyable-id button { 778 | background: var(--background-alt); 779 | cursor: pointer; 780 | border-radius: 0 0.25rem 0.25rem 0; 781 | } 782 | 783 | .feature-copyable-id button:hover { 784 | background: var(--background); 785 | } 786 | 787 | .feature-copyable-id button:active { 788 | background: var(--background-alt); 789 | } 790 | -------------------------------------------------------------------------------- /site/_data/featureCatalog.yml: -------------------------------------------------------------------------------- 1 | Structure content with HTML: 2 | Structure the document: 3 | - html 4 | - head 5 | - title 6 | - meta 7 | - base 8 | - link 9 | - body 10 | - lang-attr 11 | Create sections of content: 12 | - main 13 | - section 14 | - aside 15 | - article 16 | - nav 17 | - header-footer 18 | - dialog 19 | - menu 20 | - div 21 | - hr 22 | - details 23 | - details-name 24 | - progress 25 | Structure and format text: 26 | - headings 27 | - hgroup 28 | - p 29 | - br 30 | - a 31 | - download 32 | - em 33 | - strong 34 | - span 35 | - code 36 | - address 37 | - abbr 38 | - mark 39 | - b 40 | - q 41 | - i 42 | - s 43 | - ruby 44 | - blockquote 45 | - cite 46 | - kbd 47 | - sub-sup 48 | - u 49 | - time 50 | - del 51 | - dfn 52 | - pre 53 | - small 54 | - samp 55 | - bdi 56 | - bdo 57 | - data 58 | - ins 59 | - wbr 60 | - translate 61 | Mark up forms: 62 | - form 63 | - input 64 | - input-button 65 | - input-checkbox 66 | - input-color 67 | - input-date-time 68 | - input-email-tel-url 69 | - input-file 70 | - input-hidden 71 | - input-image 72 | - input-number 73 | - input-password 74 | - input-radio 75 | - input-range 76 | - input-reset 77 | - input-selectors 78 | - input-submit 79 | - input-date-time 80 | - input-email-tel-url 81 | - fieldset 82 | - button 83 | - search-input-type 84 | - label 85 | - search 86 | - select 87 | - customizable-select 88 | - textarea 89 | - output 90 | - datalist 91 | Mark up lists: 92 | - list-elements 93 | - description-list 94 | Mark up tabular data: 95 | - table 96 | - table-discouraged 97 | Mark up mathematical expressions: 98 | - mathml 99 | - var 100 | Embed media: 101 | - audio 102 | - canvas 103 | - figure 104 | - img 105 | - source 106 | - picture 107 | - video 108 | Embed external content: 109 | - iframe 110 | - iframe-srcdoc 111 | - iframe-credentialless 112 | - embed 113 | - object 114 | Position content: 115 | - position 116 | - physical-properties 117 | - logical-properties 118 | - absolute-positioning 119 | - grid 120 | - subgrid 121 | - grid-animation 122 | - fixed-positioning 123 | - flexbox 124 | - flexbox-gap 125 | - masonry 126 | - static-positioning 127 | - sticky-positioning 128 | - relative-positioning 129 | - anchor-positioning 130 | - multi-column 131 | - column-breaks 132 | - column-fill 133 | - column-span 134 | - column-pseudo 135 | - z-index 136 | - popover 137 | - widows-orphans 138 | - safe-area-inset 139 | - alignment-baseline 140 | - baseline-shift 141 | - baseline-source 142 | - reading-flow 143 | - visual-viewport 144 | - barprop 145 | - vertical-align 146 | - align-content-block 147 | Style content: 148 | Load CSS stylesheets: 149 | - style 150 | - link 151 | - import 152 | - css-modules 153 | Organize stylesheets: 154 | - namespace 155 | - scope 156 | - supports 157 | - supports-compat 158 | - cascade-layers 159 | - custom-properties 160 | - nesting 161 | - registered-custom-properties 162 | - unset-value 163 | - revert-value 164 | - all 165 | - inherit-value 166 | - initial-value 167 | Select elements to style: 168 | - selectors 169 | - root 170 | - backdrop 171 | - file-selector-button 172 | - first-letter 173 | - first-line 174 | - marker 175 | - placeholder 176 | - selection 177 | - spelling-grammar-error 178 | - target-text 179 | - autofill 180 | - default 181 | - dir-pseudo 182 | - focus-visible 183 | - has 184 | - indeterminate 185 | - is 186 | - modal 187 | - nth-child 188 | - nth-child-of 189 | - placeholder-shown 190 | - read-write-pseudos 191 | - state 192 | - user-pseudos 193 | - where 194 | - not 195 | - nth-of-type 196 | - time-relative-selectors 197 | - case-sensitive-attributes 198 | - case-insensitive-attributes 199 | - lang 200 | Generate style-only content: 201 | - content 202 | - attr-contents 203 | - before-after 204 | - alt-text-generated-content 205 | - details-content 206 | Define colors: 207 | - color-mix 208 | - color-scheme 209 | - color-function 210 | - rgb 211 | - currentcolor 212 | - named-color 213 | - oklab 214 | - relative-color 215 | - system-color 216 | - lab 217 | - hsl 218 | - hwb 219 | - colrv1 220 | - forced-colors 221 | - light-dark 222 | - color-contrast 223 | - contrast-color 224 | - color 225 | - color-adjust 226 | - document-colors 227 | Style elements: 228 | Set dimensions: 229 | - width-height 230 | - box-sizing 231 | - min-max-width-height 232 | - min-max-content 233 | - min-max-clamp 234 | - margin 235 | - margin-trim 236 | - padding 237 | - resize 238 | - container-queries 239 | - container-style-queries 240 | - fit-content 241 | - fit-content-function 242 | - overflow-shorthand 243 | - overflow-clip-margin 244 | - window-controls-overlay 245 | - aspect-ratio 246 | - zoom 247 | Style borders: 248 | - borders 249 | - border-image 250 | - border-radius 251 | - element 252 | - paint 253 | - outline 254 | - outlines 255 | - image-function 256 | - box-decoration-break 257 | Style background: 258 | - background 259 | - background-position 260 | - background-origin 261 | - background-image 262 | - background-attachment 263 | - background-blend-mode 264 | - background-clip 265 | - background-clip-border-area 266 | - background-clip-text 267 | - background-color 268 | - background-repeat 269 | - background-size 270 | - cross-fade 271 | - conic-gradients 272 | - gradient-interpolation 273 | - gradients 274 | - element 275 | - paint 276 | - image-orientation 277 | - image-rendering 278 | - crisp-edges 279 | - smooth 280 | - object-fit 281 | - object-position 282 | - object-view-box 283 | - image-function 284 | Transform elements: 285 | - transforms2d 286 | - transforms3d 287 | - individual-transforms 288 | - transform-box 289 | Style lists: 290 | - list-style 291 | - counter-set 292 | - counters 293 | - counter-style 294 | - counter-reset-reversed 295 | Set visibility: 296 | - two-value-display 297 | - opacity 298 | - visibility 299 | - display 300 | - display-flow-root 301 | - display-list-item 302 | - display-ruby 303 | - display-table 304 | - display-contents 305 | - content-visibility 306 | Apply effects: 307 | - masks 308 | - mask-border 309 | - mask-type 310 | - clip-path-boxes 311 | - clip-path 312 | - path-shape 313 | - shape-outside 314 | - shapes 315 | - backdrop-filter 316 | - mix-blend-mode 317 | - box-shadow 318 | Contain styles: 319 | - contain-inline-size 320 | - contain-layout 321 | - contain-size 322 | - contain-style 323 | - contain-paint 324 | - contain-intrinsic-size 325 | Control accessibility: 326 | - speak 327 | - speak-as 328 | Other: 329 | - cursor 330 | - isolation 331 | - paint-order 332 | - path-shape 333 | - overflow-anchor 334 | - touch-action 335 | Style text: 336 | Control text: 337 | - hanging-punctuation 338 | - line-break 339 | - dominant-baseline 340 | - line-clamp 341 | - line-height 342 | - letter-spacing 343 | - initial-letter 344 | - tab-size 345 | - text-align 346 | - text-align-last 347 | - text-decoration 348 | - text-decoration-selection 349 | - text-emphasis 350 | - text-indent 351 | - text-indent-each-line 352 | - text-indent-hanging 353 | - text-justify 354 | - text-size-adjust 355 | - text-spacing-trim 356 | - text-transform 357 | - text-wrap 358 | - text-wrap-mode 359 | - text-wrap-style 360 | - text-wrap-balance 361 | - text-wrap-nowrap 362 | - text-wrap-pretty 363 | - text-wrap-stable 364 | - text-box 365 | - word-break 366 | - word-break-auto-phrase 367 | - word-spacing 368 | - overflow-wrap 369 | - writing-mode 370 | - white-space 371 | - white-space-collapse 372 | - caret-color 373 | - user-select 374 | - text-overflow 375 | - custom-ellipses 376 | - hyphenate-character 377 | - hyphenate-limit-chars 378 | - hyphens 379 | - quotes 380 | - highlight 381 | - rhythmic-sizing 382 | - text-shadow 383 | - text-underline-position 384 | - text-orientation 385 | - text-stroke-fill 386 | - text-underline-offset 387 | - text-autospace 388 | - text-combine-upright 389 | - text-decoration-line-blink 390 | Ruby: 391 | - ruby-align 392 | - ruby-overhang 393 | - ruby-position 394 | Fonts: 395 | - font-family-system 396 | - font-metric-overrides 397 | - font-family-math 398 | - font-face 399 | - font-loading 400 | - font-shorthand 401 | - font-display 402 | - font-family 403 | - font-feature-settings 404 | - font-kerning 405 | - font-language-override 406 | - font-optical-sizing 407 | - font-palette 408 | - font-palette-animation 409 | - font-size 410 | - font-size-adjust 411 | - font-stretch 412 | - font-style 413 | - font-synthesis 414 | - font-synthesis-position 415 | - font-synthesis-small-caps 416 | - font-synthesis-style 417 | - font-synthesis-weight 418 | - font-variant 419 | - font-variant-alternates 420 | - font-variant-caps 421 | - font-variant-east-asian 422 | - font-variant-emoji 423 | - font-variant-ligatures 424 | - font-variant-numeric 425 | - font-variant-position 426 | - font-variation-settings 427 | - font-weight 428 | - font-family-ui 429 | - font-width 430 | Style scrolling: 431 | - scroll-snap 432 | - scroll-behavior 433 | - scrollbar-color 434 | - scrollbar-gutter 435 | - scrollbar-width 436 | - overscroll-behavior 437 | Animate elements: 438 | Declaratively: 439 | - clip-path-animatable 440 | - animation-composition 441 | - animations-css 442 | - cubic-bezier-easing 443 | - transition-behavior 444 | - transitions 445 | - linear-easing 446 | - motion-path 447 | - scroll-driven-animations 448 | - steps-easing 449 | - will-change 450 | - display-animation 451 | - overlay 452 | - starting-style 453 | - smil-svg-animations 454 | Programmatically: 455 | - web-animations 456 | - scroll-into-view 457 | - scroll-to-text-fragment 458 | - request-animation-frame 459 | - request-animation-frame-workers 460 | Animate page navigation: 461 | - view-transitions 462 | - cross-document-view-transitions 463 | - active-view-transition 464 | - view-transition-class 465 | Style forms: 466 | - accent-color 467 | - appearance 468 | - vertical-form-controls 469 | - field-sizing 470 | - tabindex 471 | Define lengths: 472 | - cap 473 | - ch 474 | - em-unit 475 | - rem 476 | - ex 477 | - ic 478 | - lh 479 | - q-unit 480 | - rcap 481 | - rch 482 | - rex 483 | - ric 484 | - rlh 485 | - viewport-unit-variants 486 | - viewport-units 487 | - calc-size 488 | Calculate styling values: 489 | - abs-sign 490 | - round-mod-rem 491 | - exp-functions 492 | - trig-functions 493 | - calc 494 | - calc-constants 495 | - attr 496 | Adapt to the device: 497 | - media-queries 498 | - media-query-range-syntax 499 | - overflow 500 | - video-dynamic-range 501 | - update 502 | - display-mode 503 | - color-gamut 504 | - dynamic-range 505 | - interaction 506 | - resolution 507 | - resolution-compat 508 | - image-set 509 | - scripting 510 | Adapt to user preferences: 511 | - prefers-color-scheme 512 | - prefers-contrast 513 | - prefers-reduced-data 514 | - prefers-reduced-motion 515 | - prefers-reduced-transparency 516 | - inverted-colors 517 | Handle media: 518 | Capture media: 519 | - html-media-capture 520 | - media-capture 521 | Display images: 522 | - avif 523 | - jpegxl 524 | - webp 525 | - svg 526 | - svg-discouraged 527 | - preloading-responsive-images 528 | - loading-lazy 529 | - image-maps 530 | - createimagebitmap 531 | - srcset 532 | Play audio and video: 533 | - audio 534 | - video 535 | - web-audio 536 | - text-tracks 537 | - audio-session 538 | - audio-video-tracks 539 | - audio-worklet 540 | - controls-list 541 | - preserves-pitch 542 | - webvtt 543 | - webvtt-cue-alignment 544 | - webvtt-regions 545 | - capture-stream-audio-video 546 | - request-video-frame-callback 547 | - webcodecs 548 | - fast-seek 549 | - speech-recognition 550 | - speech-synthesis 551 | - picture-in-picture 552 | - remote-playback 553 | - managed-media-source 554 | - media-pseudos 555 | - media-session 556 | - media-source 557 | - offline-audio-context 558 | - time-relative-selectors 559 | Draw graphics: 560 | - canvas-2d 561 | - canvas-2d-alpha 562 | - canvas-2d-willreadfrequently 563 | - canvas-context-lost 564 | - offscreen-canvas 565 | - capture-stream-canvas 566 | - canvas-createconicgradient 567 | - canvas-reset 568 | - canvas-roundrect 569 | - webgl 570 | - webgl-oes-draw-buffers-indexed 571 | - webgl2 572 | - svg 573 | - svg-filters 574 | - opacity-svg 575 | - canvas-2d-color-management 576 | - webgl-color-management 577 | - webgl2-color-management 578 | - canvas-2d-desynchronized 579 | - webgl-desynchronized 580 | - webgl2-desynchronized 581 | - webgl-color-buffer-float 582 | - webgl-compressed-texture-astc 583 | - webgl-compressed-texture-etc 584 | - webgl-compressed-texture-etc1 585 | - webgl-compressed-texture-pvrtc 586 | - webgl-compressed-texture-s3tc 587 | - webgl-compressed-texture-s3tc-srgb 588 | - webgl-debug-renderer-info 589 | - webgl-debug-shaders 590 | - webgl-depth-texture 591 | - webgl-draw-buffers 592 | - webgl-lose-context 593 | - webgl-multi-draw 594 | - webgl-sab 595 | - khr-parallel-shader-compile 596 | - angle-instanced-arrays 597 | - oes-element-index-uint 598 | - oes-fbo-render-mipmap 599 | - oes-standard-derivatives 600 | - oes-texture-float 601 | - oes-texture-float-linear 602 | - oes-texture-half-float 603 | - oes-texture-half-float-linear 604 | - oes-vertex-array-object 605 | - ext-blend-minmax 606 | - ext-color-buffer-float 607 | - ext-color-buffer-half-float 608 | - ext-disjoint-timer-query 609 | - ext-float-blend 610 | - ext-frag-depth 611 | - ext-shader-texture-lod 612 | - ext-srgb 613 | - ext-texture-compression-bptc 614 | - ext-texture-compression-rgtc 615 | - ext-texture-filter-anisotropic 616 | - ext-texture-norm16 617 | - ovr-multiview2 618 | Run client-side logic: 619 | Write JavaScript code: 620 | Write code: 621 | - javascript 622 | - let-const 623 | - nullish-coalescing 624 | - globalthis 625 | - exponentiation 626 | - error-cause 627 | - with 628 | - template-literals 629 | - arguments-callee 630 | Organize code: 631 | - script 632 | - js-modules 633 | - js-modules-service-workers 634 | - js-modules-shared-workers 635 | - js-modules-workers 636 | Asynchronous code: 637 | - promise 638 | - promise-finally 639 | - promise-allsettled 640 | - promise-any 641 | - promise-try 642 | - promise-withresolvers 643 | Reflection: 644 | - proxy-reflect 645 | Collections: 646 | - map 647 | - set 648 | - set-methods 649 | - weakmap 650 | - weakset 651 | - iterator-methods 652 | - iterators 653 | - array 654 | - array-at 655 | - array-by-copy 656 | - array-copywithin 657 | - array-fill 658 | - array-find 659 | - array-findlast 660 | - array-flat 661 | - array-group 662 | - array-includes 663 | - array-iteration-methods 664 | - array-iterators 665 | - array-splice 666 | - array-from 667 | - array-fromasync 668 | - array-isarray 669 | - array-of 670 | - stable-array-sort 671 | - typed-array-iteration-methods 672 | - typed-array-iterators 673 | - typed-arrays 674 | Strings: 675 | - strings 676 | - string-at 677 | - string-codepoint 678 | - string-includes 679 | - string-wellformed 680 | - string-matchall 681 | - string-normalize 682 | - string-pad 683 | - string-raw 684 | - string-repeat 685 | - string-replaceall 686 | - string-startsends-with 687 | - string-trim-startend 688 | Dates: 689 | - date 690 | - temporal 691 | URLs: 692 | - url 693 | - url-canparse 694 | - urlpattern 695 | Regular expressions: 696 | - regexp 697 | - regexp-compile 698 | - regexp-escape 699 | - regexp-static-properties 700 | Internationalization: 701 | - intl 702 | - intl-display-names 703 | - intl-duration-format 704 | - intl-list-format 705 | - intl-locale 706 | - intl-locale-info 707 | - intl-plural-rules 708 | - intl-relative-time-format 709 | - intl-segmenter 710 | - language 711 | Other built-ins: 712 | - navigator 713 | - number 714 | - async-await 715 | - async-generators 716 | - async-iterators 717 | - async-iterable-streams 718 | - aborting 719 | - abortsignal-any 720 | - atomics-wait-async 721 | - bigint 722 | - bigint64array 723 | - class-syntax 724 | - float16array 725 | - generators 726 | - json 727 | - json-raw 728 | - resizable-buffers 729 | - setinterval 730 | - settimeout 731 | - shared-memory 732 | - symbol 733 | - weak-references 734 | - functions 735 | - base64encodedecode 736 | Manipulate the DOM: 737 | - dom 738 | - mutation-events 739 | - mutationobserver 740 | - move-before 741 | - getboxquads 742 | - gethtml 743 | - dom-geometry 744 | - document-write 745 | - domparser 746 | Store data: 747 | - storage-access 748 | - storage-buckets 749 | - storage-manager 750 | - shared-storage 751 | - localstorage 752 | - non-cookie-storage-access 753 | - dataset 754 | - indexeddb 755 | Handle user input: 756 | - autofocus 757 | - inert 758 | - document-caretpositionfrompoint 759 | - element-from-point 760 | - show-picker-input 761 | - show-picker-select 762 | - selection-api 763 | - composed-ranges 764 | - keyboard-lock 765 | - keyboard-map 766 | - mouse-events 767 | - intersection-observer 768 | - intersection-observer-v2 769 | - writingsuggestions 770 | - edit-context 771 | - draganddrop 772 | - idle-detection 773 | - dirname 774 | - user-activation 775 | - pointer-lock 776 | - scrollend 777 | - input-event 778 | - hidden-until-found 779 | - permissions 780 | - touch-events 781 | - virtual-keyboard 782 | - wheel-events 783 | - change-event 784 | - events 785 | - focus-events 786 | - keyboard-events 787 | - pointer-events 788 | - pointer-events-api 789 | - resize-observer 790 | Run multi-threaded code: 791 | - service-workers 792 | - broadcast-channel 793 | - postmessage 794 | - channel-messaging 795 | - messageerror 796 | - structured-clone 797 | - serializable-errors 798 | - web-locks 799 | - transferable-arraybuffer 800 | Run native code: 801 | - wasm 802 | - wasm-bigint 803 | - wasm-bulk-memory 804 | - wasm-exception-handling 805 | - wasm-extended-constant-expressions 806 | - wasm-garbage-collection 807 | - wasm-multi-memory 808 | - wasm-multi-value 809 | - wasm-mutable-globals 810 | - wasm-non-trapping-float-to-int 811 | - wasm-reference-types 812 | - wasm-sign-extension-operators 813 | - wasm-simd-relaxed 814 | - wasm-tail-call-optimization 815 | - wasm-threads 816 | - wasm-exnref-exceptions 817 | - wasm-memory64 818 | - wasm-string-builtins 819 | - wasm-typed-fun-refs 820 | Handle web payments: 821 | - payment-handler 822 | - payment-request 823 | Other: 824 | - compression-streams 825 | - webauthn 826 | - webauthn-public-key-easy 827 | - web-cryptography 828 | - wasm-simd 829 | - web-otp 830 | - check-visibility 831 | - get-computed-style 832 | - requestidlecallback 833 | Integrate with the system: 834 | Integrate with the OS: 835 | - contact-picker 836 | - fullscreen 837 | - notifications 838 | - document-picture-in-picture 839 | - eyedropper 840 | - registerprotocolhandler 841 | - app-file-handlers 842 | - app-protocol-handlers 843 | - app-share-targets 844 | - app-shortcuts 845 | - share 846 | - virtual-keyboard 847 | - badging 848 | Access hardware: 849 | Sensors: 850 | - orientation-sensor 851 | - gyroscope 852 | - geolocation 853 | - screen-orientation 854 | - screen-orientation-lock 855 | - screen-wake-lock 856 | - device-orientation-events 857 | - accelerometer 858 | - device-posture 859 | - magnetometer 860 | - ambient-light 861 | Chips and peripherals: 862 | - web-bluetooth 863 | - webgpu 864 | - webusb 865 | - web-midi 866 | - web-nfc 867 | - webnn 868 | - webhid 869 | - battery 870 | - compute-pressure 871 | - presentation-api 872 | - ink 873 | - hardware-concurrency 874 | - vibration 875 | - gamepad 876 | - gamepad-haptics 877 | - gamepad-touch 878 | - gamepad-vr 879 | - device-memory 880 | XR: 881 | - webxr-device 882 | - webxr-ar 883 | - webxr-depth-sensing 884 | - webxr-dom-overlays 885 | - webxr-gamepads 886 | - webxr-layers 887 | - webxr-hand-input 888 | - webxr-anchors 889 | - webxr-lighting-estimation 890 | - webxr-camera 891 | - webxr-hit-test 892 | Access files: 893 | - file-system-access 894 | - origin-private-file-system 895 | Access system clipboard: 896 | - async-clipboard 897 | - clipboard-events 898 | - clipboard-supports 899 | - clipboard-custom-format 900 | - clipboard-unsanitized-formats 901 | - clipboard-svg 902 | Install site as app: 903 | - beforeinstallprompt 904 | - manifest 905 | - window-controls-overlay 906 | - service-workers 907 | - app-launch-handler 908 | - notifications-apps 909 | - meta-application-title 910 | Create UI components: 911 | Mark up components: 912 | - slot 913 | - template 914 | - declarative-shadow-dom 915 | Customize components: 916 | - shadow-dom 917 | - shadow-parts 918 | - autonomous-custom-elements 919 | - form-associated-custom-elements 920 | - customized-built-in-elements 921 | - slot-assign 922 | - closewatcher 923 | Style UI components: 924 | - host 925 | - host-context 926 | - constructed-stylesheets 927 | Communicate over the network: 928 | Download resources: 929 | - http11 930 | - http2 931 | - http3 932 | - webtransport 933 | - fetch 934 | - fetch-metadata 935 | - fetch-priority 936 | - fetchlater 937 | - fetch-request-streams 938 | - abortable-fetch 939 | - xhr 940 | - font-loading 941 | - import 942 | - json-modules 943 | - background-fetch 944 | - background-sync 945 | - modulepreload 946 | - loading-lazy 947 | - referrer-policy 948 | - import-maps 949 | - blocking-render 950 | - pdf-viewer 951 | - download 952 | Compress resources: 953 | - compression-dictionary-transport 954 | - brotli 955 | - zstd 956 | Open communication channels: 957 | - websockets 958 | - webrtc 959 | - webrtc-encoded-transform 960 | - webrtc-sctp 961 | - server-sent-events 962 | - push 963 | - streams 964 | Handle connection state: 965 | - online 966 | Handle page navigation: 967 | - hashchange 968 | - history 969 | - navigation 970 | - beforeunload 971 | Print webpages: 972 | Control print: 973 | - print 974 | - print-color-adjust 975 | - print-events 976 | Style printed pages: 977 | - page-breaks 978 | - page-selectors 979 | - page-setup 980 | - page-orientation 981 | Measure performance: 982 | - performance 983 | - profiler 984 | - largest-contentful-paint 985 | - paint-timing 986 | - element-timing 987 | - event-timing 988 | - navigation-timing 989 | - measure-memory 990 | - resource-size 991 | - resource-timing 992 | - server-timing 993 | - bfcache-blocking-reasons 994 | - longtasks 995 | - layout-instability 996 | - page-visibility-state 997 | - long-animation-frames 998 | - performancetiming 999 | Test: 1000 | - webdriver 1001 | - webdriver-bidi 1002 | - virtual-pressure-sources 1003 | - virtual-sensors 1004 | - console 1005 | -------------------------------------------------------------------------------- /.eleventy.js: -------------------------------------------------------------------------------- 1 | import { EleventyHtmlBasePlugin } from "@11ty/eleventy"; 2 | import feedPlugin from "@11ty/eleventy-plugin-rss"; 3 | import YAML from 'yaml'; 4 | import { browsers, features as webFeaturesFeatures, groups } from "web-features"; 5 | import bcd from "@mdn/browser-compat-data" with { type: "json" }; 6 | import specs from "browser-specs" with { type: "json" }; 7 | import mdnInventory from "@ddbeck/mdn-content-inventory"; 8 | import mdnDocsOverrides from "./additional-data/mdn-docs.json" with { type: "json" }; 9 | import standardPositions from "./additional-data/standard-positions.json" with { type: "json" }; 10 | import originTrials from "./additional-data/origin-trials.json" with { type: "json" }; 11 | import stateOfSurveys from "./additional-data/state-of-surveys.json" with { type: "json" }; 12 | import useCounters from "./additional-data/use-counters.json" with { type: "json" }; 13 | import interop from "./additional-data/interop.json" with { type: "json" }; 14 | import wpt from "./additional-data/wpt.json" with { type: "json" }; 15 | 16 | // Number of months after Baseline low that Baseline high happens. 17 | // Keep in sync with definition at: 18 | // https://github.com/web-platform-dx/web-features/blob/main/docs/baseline.md#wider-support-high-status 19 | const BASELINE_LOW_TO_HIGH_DURATION = 30; 20 | 21 | const BROWSER_BUG_TRACKERS = { 22 | chrome: "issues.chromium.org", 23 | chrome_android: "issues.chromium.org", 24 | edge: "issues.chromium.org", 25 | firefox: "bugzilla.mozilla.org", 26 | firefox_android: "bugzilla.mozilla.org", 27 | safari: "bugs.webkit.org", 28 | safari_ios: "bugs.webkit.org", 29 | }; 30 | 31 | const MDN_URL_ROOT = "https://developer.mozilla.org/docs/"; 32 | 33 | function getAllBCDKeys() { 34 | function walk(root, acc, keyPrefix = "") { 35 | for (const key in root) { 36 | if (!keyPrefix && (key === "__meta" || key === "browsers" || key === "webextensions")) { 37 | continue; 38 | } 39 | 40 | if (key === "__compat") { 41 | acc.push({ key: keyPrefix, status: root[key].status }); 42 | } 43 | 44 | if (key !== "__compat" && typeof root[key] === "object") { 45 | const bcdKey = keyPrefix ? `${keyPrefix}.${key}` : key; 46 | walk(root[key], acc, bcdKey); 47 | } 48 | } 49 | } 50 | 51 | const keys = []; 52 | walk(bcd, keys); 53 | 54 | return keys; 55 | } 56 | 57 | function findParentGroupId(group) { 58 | if (!group.parent) { 59 | return null; 60 | } 61 | 62 | return group.parent; 63 | } 64 | 65 | function stripLessThan(dateStr) { 66 | if (dateStr.startsWith("≤")) { 67 | return dateStr.substring(1); 68 | } 69 | return dateStr; 70 | } 71 | 72 | function augmentFeatureData(feature) { 73 | // Make rename group to groups, makes more sense since it's always an array. 74 | feature.groups = feature.group || []; 75 | 76 | // Create group paths. The groups that a feature belongs to might be 77 | // nested in parent groups. 78 | feature.groupPaths = []; 79 | for (const groupId of feature.groups) { 80 | const path = [groups[groupId].name]; 81 | let currentGroupId = groupId; 82 | 83 | while (true) { 84 | const parentId = findParentGroupId(groups[currentGroupId]); 85 | if (!parentId) { 86 | break; 87 | } 88 | path.unshift(groups[parentId].name); 89 | currentGroupId = parentId; 90 | } 91 | feature.groupPaths.push(path); 92 | } 93 | 94 | // Add spec data from browser-specs, when possible. 95 | feature.spec = feature.spec.map((spec) => { 96 | if (!spec) { 97 | return null; 98 | } 99 | 100 | const fragment = spec.includes("#") ? spec.split("#")[1] : null; 101 | // Look for the spec URL in the browser-specs data. 102 | const specData = specs.find((specData) => { 103 | return ( 104 | specData.url === spec || 105 | spec.startsWith(specData.url) || 106 | (specData.nightly && spec.startsWith(specData.nightly.url)) 107 | ); 108 | }); 109 | return specData ? { ...specData, url: spec, fragment } : { url: spec }; 110 | }).filter((spec) => !!spec); 111 | 112 | // Get the first part of each BCD key in the feature (e.g. css, javascript, html, api, ...) 113 | // to use as tags. 114 | const bcdTags = []; 115 | 116 | const bcdKeysData = (feature.compat_features || []) 117 | .map((key) => { 118 | // Find the BCD entry for this key. 119 | const keyParts = key.split("."); 120 | 121 | let data = bcd; 122 | for (const part of keyParts) { 123 | if (!data || !data[part]) { 124 | console.warn( 125 | `No BCD data for ${key}. Check if the web-features and browser-compat-data dependencies are in sync.` 126 | ); 127 | return null; 128 | } 129 | data = data[part]; 130 | } 131 | 132 | bcdTags.push(keyParts[0]); 133 | 134 | return data && data.__compat ? { key, compat: data.__compat } : null; 135 | }) 136 | .filter((data) => !!data); 137 | 138 | // Add MDN doc links, if any. 139 | const mdnUrls = []; 140 | 141 | if (mdnDocsOverrides[feature.id] && mdnDocsOverrides[feature.id].length) { 142 | // If the feature has a doc override, use that. 143 | for (const slug of mdnDocsOverrides[feature.id]) { 144 | const slugParts = slug.split("#"); 145 | const hasAnchor = slugParts.length > 1; 146 | 147 | const mdnArticleData = mdnInventory.inventory.find((item) => { 148 | return item.frontmatter.slug === (hasAnchor ? slugParts[0] : slug); 149 | }); 150 | if (mdnArticleData) { 151 | mdnUrls.push({ 152 | title: mdnArticleData.frontmatter.title, 153 | anchor: hasAnchor ? slugParts[1] : null, 154 | url: MDN_URL_ROOT + mdnArticleData.frontmatter.slug + (hasAnchor ? `#${slugParts[1]}` : ""), 155 | }); 156 | } 157 | } 158 | } else { 159 | // Otherwise, compute a list of MDN docs based on BCD keys. 160 | for (const { key, mdn_url } of bcdKeysData) { 161 | const mdnArticleData = mdnInventory.inventory.find((item) => { 162 | return item.frontmatter["browser-compat"] === key; 163 | }); 164 | if (mdnArticleData) { 165 | mdnUrls.push({ 166 | title: mdnArticleData.frontmatter.title, 167 | url: MDN_URL_ROOT + mdnArticleData.frontmatter.slug, 168 | }); 169 | } else if (mdn_url) { 170 | mdnUrls.push({ 171 | url: mdn_url, 172 | title: mdn_url, 173 | }); 174 | } 175 | } 176 | } 177 | 178 | feature.mdnUrls = mdnUrls; 179 | 180 | // Add standard positions. 181 | feature.standardPositions = standardPositions[feature.id]; 182 | feature.hasNegativeStandardPosition = 183 | feature?.standardPositions?.mozilla?.position === "negative" || 184 | feature?.standardPositions?.webkit?.position === "oppose"; 185 | 186 | // Add origin trials. 187 | feature.originTrials = originTrials[feature.id]; 188 | 189 | // Add state of surveys data. 190 | feature.stateOfSurveys = stateOfSurveys[feature.id]; 191 | 192 | // Add use counter data. 193 | feature.useCounters = useCounters[feature.id]; 194 | 195 | // Add interop data. 196 | feature.interop = []; 197 | for (const interopYear in interop) { 198 | for (const interopLabel in interop[interopYear]) { 199 | const interopFeatures = interop[interopYear][interopLabel]; 200 | if (interopFeatures.includes(feature.id)) { 201 | feature.interop.push({ 202 | year: interopYear, 203 | label: interopLabel, 204 | }); 205 | } 206 | } 207 | } 208 | 209 | // Add WPT data, if any. 210 | feature.wpt = wpt.includes(feature.id); 211 | 212 | // Add the BCD data to the feature. 213 | feature.bcdData = bcdKeysData; 214 | feature.bcdTags = [...new Set(bcdTags)]; 215 | 216 | // Add the baseline low and high dates as JS objects too. 217 | feature.baselineLowDateAsObject = feature.status.baseline 218 | ? new Date(stripLessThan(feature.status.baseline_low_date)) 219 | : null; 220 | feature.baselineHighDateAsObject = feature.status.baseline && feature.status.baseline === "high" 221 | ? new Date(stripLessThan(feature.status.baseline_high_date)) 222 | : null; 223 | 224 | // Add expected baseline high date, if applicable. 225 | if (feature.status.baseline === "low") { 226 | // If the feature is baseline low, then we expect it to become baseline high 227 | // in BASELINE_LOW_TO_HIGH_DURATION months. 228 | const baselineLowDate = feature.baselineLowDateAsObject; 229 | if (baselineLowDate) { 230 | const expectedBaselineHighDate = new Date(baselineLowDate); 231 | expectedBaselineHighDate.setMonth( 232 | expectedBaselineHighDate.getMonth() + BASELINE_LOW_TO_HIGH_DURATION 233 | ); 234 | feature.expectedBaselineHighDate = expectedBaselineHighDate.toISOString().substring(0, 10); 235 | } else { 236 | feature.expectedBaselineHighDate = null; 237 | } 238 | } 239 | 240 | // Add impl_url links, if any, per browser. 241 | const browserImplUrls = Object.keys(browsers).reduce((acc, browserId) => { 242 | acc[browserId] = []; 243 | return acc; 244 | }, {}); 245 | 246 | for (const { compat } of bcdKeysData) { 247 | for (const browserId in browsers) { 248 | const browserSupport = compat.support[browserId]; 249 | if (!browserSupport.version_added && browserSupport.impl_url) { 250 | browserImplUrls[browserId] = [ 251 | ...new Set([...browserImplUrls[browserId], browserSupport.impl_url]), 252 | ]; 253 | } 254 | } 255 | } 256 | 257 | feature.implUrls = browserImplUrls; 258 | } 259 | 260 | // Massage the web-features data so it's more directly useful in our 11ty templates. 261 | const features = {}; 262 | const unordinaryFeatures = {}; 263 | 264 | for (const id in webFeaturesFeatures) { 265 | const feature = webFeaturesFeatures[id]; 266 | // Add the id to the object. 267 | feature.id = id; 268 | 269 | // Only add our custom data to each feature for ordinary features. 270 | // Skip kind:split and kind:moved. These are handled differently in the templates. 271 | if (feature.kind === "feature") { 272 | augmentFeatureData(feature); 273 | 274 | // Store the newly augmented feature data. 275 | features[id] = feature; 276 | } else { 277 | // Store unordinary features separately. 278 | unordinaryFeatures[id] = feature; 279 | } 280 | } 281 | 282 | function getBrowserVersionReleaseDate(browser, version) { 283 | const isBeforeThan = version.startsWith("≤"); 284 | const cleanVersion = isBeforeThan ? version.substring(1) : version; 285 | const date = browser.releases.find( 286 | (release) => release.version === cleanVersion 287 | ).date; 288 | 289 | return { 290 | isBeforeThan, 291 | date 292 | }; 293 | } 294 | 295 | export default function (eleventyConfig) { 296 | eleventyConfig.addPlugin(EleventyHtmlBasePlugin); 297 | 298 | eleventyConfig.addPassthroughCopy("site/assets"); 299 | eleventyConfig.addPassthroughCopy({ "node_modules/apexcharts/dist/apexcharts.css": "assets/apexcharts.css" }); 300 | eleventyConfig.addPassthroughCopy({ "node_modules/apexcharts/dist/apexcharts.min.js": "assets/apexcharts.js" }); 301 | 302 | eleventyConfig.addDataExtension("yml,yaml", (contents, filePath) => { 303 | return YAML.parse(contents); 304 | }); 305 | 306 | eleventyConfig.addShortcode( 307 | "browserVersionRelease", 308 | function (browser, version) { 309 | const { isBeforeThan, date } = getBrowserVersionReleaseDate(browser, version); 310 | return isBeforeThan ? `Released before ${date}` : `Released on ${date}`; 311 | } 312 | ); 313 | 314 | eleventyConfig.addShortcode("prettyFeatureName", function (name) { 315 | if (name.startsWith("`") && name.endsWith("`")) { 316 | return `${name.substring(1, name.length - 1)}`; 317 | } 318 | 319 | return name.replace(//g, ">"); 320 | }); 321 | 322 | eleventyConfig.addShortcode("escapeJSON", function (name) { 323 | return name.replace(/"/g, "\\\""); 324 | }); 325 | 326 | eleventyConfig.addShortcode("baselineDate", function (dateStr) { 327 | const isBefore = dateStr.startsWith("≤"); 328 | if (isBefore) { 329 | return `before ${dateStr.substring(1)}`; 330 | } 331 | return dateStr; 332 | }); 333 | 334 | eleventyConfig.addShortcode("useCounterPercentage", function (value) { 335 | return `~${(value * 100).toFixed(3)}%`; 336 | }); 337 | 338 | eleventyConfig.addFilter("stringify", (data) => { 339 | return JSON.stringify(data, null, " ", 2) 340 | }) 341 | 342 | eleventyConfig.addGlobalData("versions", async () => { 343 | const { default: webFeaturesPackageJson } = await import( 344 | "./node_modules/web-features/package.json", 345 | { 346 | with: { type: "json" }, 347 | } 348 | ); 349 | 350 | return { 351 | date: new Date().toLocaleDateString(), 352 | webFeatures: webFeaturesPackageJson.version, 353 | bcd: bcd.__meta.version, 354 | }; 355 | }); 356 | 357 | eleventyConfig.addGlobalData("browsers", () => { 358 | return Object.keys(browsers).map((browserId) => { 359 | return { 360 | id: browserId, 361 | name: browsers[browserId].name, 362 | releases: browsers[browserId].releases.map(release => { 363 | // Add the status of the release from BCD (current, retired, beta, nightly). 364 | release.status = bcd.browsers[browserId].releases[release.version].status; 365 | return release; 366 | }), 367 | bugTracker: BROWSER_BUG_TRACKERS[browserId], 368 | }; 369 | }); 370 | }); 371 | 372 | eleventyConfig.addGlobalData("perGroup", () => { 373 | return groups; 374 | }); 375 | 376 | eleventyConfig.addGlobalData("latest", () => { 377 | const maxLowFeatures = 5; 378 | const lowFeatures = []; 379 | const maxHighFeatures = 5; 380 | const highFeatures = []; 381 | 382 | for (const id in features) { 383 | const feature = features[id]; 384 | 385 | if (feature.status.baseline === "low") { 386 | lowFeatures.push(feature); 387 | } 388 | 389 | if (feature.status.baseline === "high") { 390 | highFeatures.push(feature); 391 | } 392 | } 393 | 394 | return { 395 | latestBaselineLow: lowFeatures.sort((a, b) => { 396 | return ( 397 | new Date(b.status.baseline_low_date) - 398 | new Date(a.status.baseline_low_date) 399 | ); 400 | }).slice(0, maxLowFeatures), 401 | latestBaselineHigh: highFeatures.sort((a, b) => { 402 | return ( 403 | new Date(b.status.baseline_high_date) - 404 | new Date(a.status.baseline_high_date) 405 | ); 406 | }).slice(0, maxHighFeatures) 407 | }; 408 | }); 409 | 410 | eleventyConfig.addGlobalData("perMonth", () => { 411 | const monthly = new Map(); 412 | 413 | const ensureMonthEntry = (month) => { 414 | if (!monthly.has(month)) { 415 | const obj = { high: [], low: [], all: new Set() }; 416 | for (const browserId in browsers) { 417 | obj[browserId] = []; 418 | } 419 | 420 | monthly.set(month, obj); 421 | } 422 | }; 423 | 424 | const getMonth = (dateStr) => { 425 | if (!dateStr) { 426 | return null; 427 | } 428 | 429 | return stripLessThan(dateStr).substring(0, 7); 430 | }; 431 | 432 | const getBaselineHighMonth = (feature) => getMonth(feature.status.baseline_high_date); 433 | const getBaselineLowMonth = (feature) => getMonth(feature.status.baseline_low_date); 434 | 435 | const getBrowserSupportMonth = (feature, browserId) => { 436 | const versionSupported = feature.status.support[browserId]; 437 | const releaseData = browsers[browserId].releases; 438 | 439 | if (!versionSupported) { 440 | return null; 441 | } 442 | 443 | const release = releaseData.find(r => r.version === versionSupported); 444 | if (!release) { 445 | return null; 446 | } 447 | 448 | return getMonth(release.date); 449 | }; 450 | 451 | for (const id in features) { 452 | const feature = features[id]; 453 | 454 | const baselineHighMonth = getBaselineHighMonth(feature); 455 | if (baselineHighMonth) { 456 | ensureMonthEntry(baselineHighMonth); 457 | monthly.get(baselineHighMonth).high.push(feature); 458 | monthly.get(baselineHighMonth).all.add(feature); 459 | } 460 | 461 | const baselineLowMonth = getBaselineLowMonth(feature); 462 | if (baselineLowMonth) { 463 | ensureMonthEntry(baselineLowMonth); 464 | monthly.get(baselineLowMonth).low.push(feature); 465 | monthly.get(baselineLowMonth).all.add(feature); 466 | } 467 | 468 | for (const browserId in browsers) { 469 | const browserSupportMonth = getBrowserSupportMonth(feature, browserId); 470 | if (browserSupportMonth) { 471 | ensureMonthEntry(browserSupportMonth); 472 | // Only record the feature if it hasn't already been recorded as baseline 473 | // low or high for the same month. 474 | const alreadyRecorded = 475 | monthly 476 | .get(browserSupportMonth) 477 | .high.some((f) => f.id === feature.id) || 478 | monthly 479 | .get(browserSupportMonth) 480 | .low.some((f) => f.id === feature.id); 481 | if (!alreadyRecorded) { 482 | monthly.get(browserSupportMonth)[browserId].push(feature); 483 | monthly.get(browserSupportMonth).all.add(feature); 484 | } 485 | } 486 | } 487 | } 488 | 489 | const now = new Date(); 490 | return [...monthly] 491 | .sort((a, b) => { 492 | return new Date(b[0]) - new Date(a[0]); 493 | }) 494 | .map((month) => { 495 | const absoluteDate = new Date(month[0]); 496 | const isCurrentMonth = 497 | absoluteDate.getMonth() === now.getMonth() && 498 | absoluteDate.getFullYear() === now.getFullYear(); 499 | return { 500 | date: new Date(month[0]).toLocaleDateString("en-us", { 501 | month: "long", 502 | year: "numeric", 503 | }), 504 | absoluteDate, 505 | // current month is not stable because it is still updating 506 | // RSS feed should not include the current month 507 | // https://github.com/web-platform-dx/web-features-explorer/pull/23 508 | isStableMonth: !isCurrentMonth, 509 | all: [...month[1].all], 510 | features: month[1], 511 | }; 512 | }); 513 | }); 514 | 515 | eleventyConfig.addGlobalData("allFeaturesAsObject", () => { 516 | return features; 517 | }); 518 | 519 | eleventyConfig.addGlobalData("allFeatures", () => { 520 | const all = []; 521 | 522 | for (const id in features) { 523 | const feature = features[id]; 524 | all.push(feature); 525 | } 526 | 527 | return all; 528 | }); 529 | 530 | eleventyConfig.addGlobalData("allUnordinaryFeatures", () => { 531 | const all = []; 532 | 533 | for (const id in unordinaryFeatures) { 534 | const feature = unordinaryFeatures[id]; 535 | all.push(feature); 536 | } 537 | 538 | return all; 539 | }); 540 | 541 | eleventyConfig.addGlobalData("allFeaturesAsJSON", () => { 542 | const all = []; 543 | 544 | for (const id in features) { 545 | const feature = features[id]; 546 | all.push({ 547 | description_html: feature.description_html, 548 | id: feature.id, 549 | name: feature.name, 550 | status: feature.status, 551 | }); 552 | } 553 | 554 | return JSON.stringify(all); 555 | }); 556 | 557 | eleventyConfig.addGlobalData("widelyAvailableFeatures", () => { 558 | const widelyAvailable = []; 559 | 560 | for (const id in features) { 561 | const feature = features[id]; 562 | 563 | // Baseline features only. 564 | if (feature.status.baseline === "high") { 565 | widelyAvailable.push(feature); 566 | } 567 | } 568 | 569 | return widelyAvailable.sort((a, b) => { 570 | // Sort by baseline_high_date, descending, so the most recent is first. 571 | return ( 572 | new Date(b.status.baseline_high_date) - 573 | new Date(a.status.baseline_high_date) 574 | ); 575 | }); 576 | }); 577 | 578 | eleventyConfig.addGlobalData("limitedAvailabilityFeatures", () => { 579 | const limitedAvailability = []; 580 | 581 | for (const id in features) { 582 | const feature = features[id]; 583 | 584 | // Non-baseline features only that are also not discouraged. 585 | if (!feature.status.baseline && !feature.discouraged) { 586 | limitedAvailability.push(feature); 587 | } 588 | } 589 | 590 | // Sort the features by date, with the most recently updated 591 | // feature first, irrespective of which browser it was updated for. 592 | return limitedAvailability.sort((a, b) => { 593 | let maxADate = 0; 594 | let maxBDate = 0; 595 | 596 | // Get all of the browser release dates for the compared features 597 | // and get the most recent. 598 | for (const browserId in browsers) { 599 | const aVersion = a.status.support[browserId]; 600 | if (aVersion) { 601 | const date = new Date(getBrowserVersionReleaseDate(browsers[browserId], aVersion).date); 602 | maxADate = Math.max(maxADate, date.getTime()); 603 | } 604 | const bVersion = b.status.support[browserId]; 605 | if (bVersion) { 606 | const date = new Date(getBrowserVersionReleaseDate(browsers[browserId], bVersion).date); 607 | maxBDate = Math.max(maxBDate, date.getTime()); 608 | } 609 | } 610 | 611 | return maxBDate - maxADate; 612 | }); 613 | }); 614 | 615 | eleventyConfig.addGlobalData("newlyAvailableFeatures", () => { 616 | const newlyAvailable = []; 617 | 618 | for (const id in features) { 619 | const feature = features[id]; 620 | 621 | // Only baseline low. 622 | if (feature.status.baseline === "low") { 623 | newlyAvailable.push(feature); 624 | } 625 | } 626 | 627 | return newlyAvailable.sort((a, b) => { 628 | // Sort by baseline_low_date, descending, so the most recent is first. 629 | return ( 630 | new Date(b.status.baseline_low_date) - 631 | new Date(a.status.baseline_low_date) 632 | ); 633 | }); 634 | }); 635 | 636 | eleventyConfig.addGlobalData("bcdMapping", () => { 637 | const mapped = []; 638 | for (const id in features) { 639 | const feature = features[id]; 640 | if (feature.compat_features && feature.compat_features.length) { 641 | mapped.push(...feature.compat_features); 642 | } 643 | } 644 | 645 | const unmapped = []; 646 | let lastKeyContext = null; 647 | getAllBCDKeys().forEach(({ key, status }) => { 648 | if (!mapped.includes(key)) { 649 | const keyContent = key.split(".").slice(0, 2).join("."); 650 | unmapped.push({ key, status, isNewGroup: keyContent !== lastKeyContext }); 651 | lastKeyContext = keyContent; 652 | } 653 | }); 654 | 655 | return { 656 | totalKeys: mapped.length + unmapped.length, 657 | totalMapped: mapped.length, 658 | unmapped, 659 | percentage: ((mapped.length / (mapped.length + unmapped.length)) * 100).toFixed(0), 660 | }; 661 | }); 662 | 663 | eleventyConfig.addGlobalData("missingOneBrowserFeatures", () => { 664 | const missingOne = []; 665 | 666 | for (const id in features) { 667 | const feature = features[id]; 668 | 669 | // Only non-baseline features. 670 | if (feature.status.baseline) { 671 | continue; 672 | } 673 | 674 | // And, out of those, only those that are missing support in just one browser (engine). 675 | const noSupport = []; 676 | for (const browserId in browsers) { 677 | if (!feature.status.support[browserId]) { 678 | noSupport.push(browserId); 679 | } 680 | } 681 | 682 | if (noSupport.length === 1) { 683 | feature.blockedOn = browsers[noSupport[0]].name; 684 | missingOne.push(feature); 685 | } 686 | 687 | if (noSupport.length === 2) { 688 | // If one of the two values is a substring of the other, then these are the same engine. 689 | const [first, second] = noSupport; 690 | if (first.includes(second) || second.includes(first)) { 691 | feature.blockedOn = browsers[noSupport[0]].name; 692 | missingOne.push(feature); 693 | } 694 | } 695 | } 696 | 697 | // Go over the features we found and add some information about the last browser 698 | // that doesn't yet support the feature. 699 | missingOne.forEach((feature) => { 700 | let mostRecent = null; 701 | 702 | const support = feature.status.support; 703 | for (const browserId in support) { 704 | let versionSupported = support[browserId]; 705 | if (versionSupported.startsWith("≤")) { 706 | versionSupported = versionSupported.substring(1); 707 | } 708 | 709 | // Grab release date string from BCD as it has a more complete list of 710 | // browser releases than the web features data. 711 | const releaseDateStr = 712 | bcd.browsers[browserId]?.releases[versionSupported]?.release_date; 713 | 714 | // Some are missing 715 | if (!releaseDateStr) { 716 | continue; 717 | } 718 | 719 | const releaseDate = new Date(releaseDateStr); 720 | if (!mostRecent) { 721 | mostRecent = releaseDate; 722 | } else { 723 | if (releaseDate > mostRecent) { 724 | mostRecent = releaseDate; 725 | } 726 | } 727 | } 728 | const today = new Date(); 729 | const formatter = new Intl.DateTimeFormat("en", { 730 | year: "numeric", 731 | month: "long", 732 | }); 733 | const monthDiff = (older, newer) => { 734 | return ( 735 | (newer.getFullYear() - older.getFullYear()) * 12 + 736 | (newer.getMonth() - older.getMonth()) 737 | ); 738 | }; 739 | 740 | feature.monthsBlocked = monthDiff(mostRecent, today); 741 | feature.blockedSince = formatter.format(mostRecent); 742 | }); 743 | 744 | // Sort the features by monthsBlocked, with the most recently 745 | // updated feature first. Most recently updated means the 746 | // shortest blocked time. 747 | return missingOne.sort((a, b) => { 748 | return a.monthsBlocked - b.monthsBlocked; 749 | }); 750 | }); 751 | 752 | // RSS Feed Plugin 753 | eleventyConfig.addPlugin(feedPlugin); 754 | eleventyConfig.addLiquidFilter("dateToRfc3339", feedPlugin.dateToRfc3339); 755 | 756 | return { 757 | dir: { 758 | input: "site", 759 | output: "docs", 760 | }, 761 | pathPrefix: "/web-features-explorer/", 762 | }; 763 | } 764 | --------------------------------------------------------------------------------