explainers

The <agecheck> element

Authors

Participate

Status

A strawperson proposal, meant to start discussion. Nothing is implemented in a shipping browser.

Introduction

Sites that check age today use either a simple link/button, or send people to third-party services or ask them to upload an ID. Uploading IDs leaks personal data and collects far more than the site needs. Sites need a way to learn “at least this age” and nothing else, without needing a third-party age service, in regular and private browsing alike.

<agecheck> is a form element that lets a site learn only one thing: whether the person is at least 13, 16, or 18. The answer is declared, not verified. The browser takes the answer from an age the person or a parent already gave the platform, asks the person before sharing it, and gives the site only yes or no.

Goals

Non-goals

The proposal

A new HTML element, <agecheck>. When the person clicks it, the browser asks the platform for the person’s age, or uses an age it stores itself. The platform may ask whether to share the age with the browser, as it does for any app, usually only the first time. Then the browser shows its own prompt, naming the site and the age. If the person can share, the browser asks whether to. If they can’t, the browser says so.

If the person agrees, the page learns that the person is at least the age the site named. Otherwise the page gets the same error as for any failure, so the browser never tells it why. The element works inside a form, like an input element.

To the server, the answer is an honor system, like today’s “I’m over 18” button. Anyone can send the same form value, so it proves nothing to a regulator that requires verification.

Why an element

The user clicks the element, the browser shows its own prompt, and the page gets only the result or an error. It works in a plain form with no script, and the browser knows what the control is, so it can give it the right role and accessible name. Only the user’s own action on it or its label opens the prompt. Script can’t, even with click().

By default, the element looks and lays out like a <button>, so a site that doesn’t style it still shows a control people recognize. Authors can restyle it or hide it behind a label, as they do other form controls. A site could label the element misleadingly, as with any button. So the browser’s prompt, not the element’s look, protects people. For example, the prompt should ignore input for a short time (see “Privacy and security”). During this time, the prompt shows that the prompt is not ready yet, so people using assistive technology do not have to guess why their input does nothing.

Embedded content

In this first version, embedded third party content, such as a video player, can only get the answer from the top-level page, by its own means, such as postMessage, so it must trust that page. A third party can run the check only as a top-level page of its own, such as a popup, where the prompt names the third party.

Where the age comes from

The browser doesn’t ask the site or a third party. When the platform has an age API, the browser uses an age the person or a parent already gave the platform, such as Apple’s Declared Age Range or Google’s Play Age Signals. Both give apps an age that the person declared, that a parent set, or that the platform verified. How the platform sets the age is up to the platform.

What the browser does:

For children, the platform’s parental controls decide whether the browser gets an age at all. On Apple platforms a parent can choose to always share, never share, or have the child asked each time. If a parent’s setting or the platform blocks sharing, the browser shows its “your age can’t be shared” prompt, the same for children and adults.

Ages

A site picks one age per check: 13, 16, or 18. Every platform age API we know of supports those ages, though some regions use their own (see “Jurisdiction”). They also match the default groups of Google’s Play Age Signals (0 to 12, 13 to 15, 16 to 17, and 18 and over), so a browser on Android needs no special setup.

A site can ask about more than one age, with one check for each age, for example a movie site with films rated 13, 16, and 18.

A site that asks again and again can see the day the answer changes. That day is the anniversary of when the age was set, not the birthday. Where the browser keeps a copy or stores the age itself, it changes on the anniversary of when the browser got it. If the person updates the age early, the change may hint at a birthday. The cost is that someone who has just had a birthday may fail until that anniversary, unless they or a parent update the age.

So by asking several ages, a site learns the highest age the person agrees to share. The site can guess the person is younger than the next age, but it can’t be sure, because it can’t tell a refusal from a person who is too young. A native app using Declared Age Range learns the person’s age range with a single request. Sites that already share data about a person can also pool what they each learned, but the answer carries little that links a person across sites.

Jurisdiction

The platform, not the browser, knows which rules apply to the person’s location. Declared Age Range uses the person’s location and account settings. The platform answers with an age range. The browser says yes only if the bottom of the range is at least the age the site asked about. Declared Age Range gives someone under 13 an age range with no bottom. An age range with no bottom counts as below every age. The browser never answers about a different age than the age the site asked about.

When the platform returns no age range, the browser treats the missing age range as not sharing instead of having no age set.

Some regions replace the ages the site asked about with their own ages. An age range can then start below and end above the age the site asked about. For example, a site asks about age 16. The platform gives an age range of 14 to 17. The browser cannot tell whether the person is at least 16 and fails the check. In those regions, some people who meet the age fail the check.

Using the element

<form method="post" action="/enter">
  <agecheck name="age" age="18" required>Share that I'm 18 or older</agecheck>
  <button>Enter</button>
</form>

Advice for sites. This section is advice that the browser does not enforce.

This code example shows a site following the advice. People who use a screen reader or only the keyboard can use the page this way.

<form method="post" action="/enter">
  <agecheck id="check" name="age" age="18" required>Share that I'm 18 or older</agecheck>
  <span id="message" role="status"></span>
  <a id="other" href="/other-ways" hidden>Show my age another way</a>
  <button>Enter</button>
</form>

<script>
  const check = document.getElementById("check");
  const message = document.getElementById("message");
  const other = document.getElementById("other");

  check.addEventListener("change", () => {
    other.hidden = true;
    message.textContent = "Thanks. You can go in now.";
  });

  check.addEventListener("error", () => {
    message.textContent = "We couldn't use your age this time. You can try again, or show your age another way.";
    other.hidden = false;
  });
</script>

The live region starts empty, so screen readers are already watching the live region when the text changes. The example page never moves focus on its own. The message says what happened.

Older browsers and repeat checks

A site can check for HTMLAgeCheckElement in window and show its old fallback if it’s missing. In a browser that doesn’t know the element, the text inside it shows as plain text. A site can’t learn in advance whether the person has an age set.

The first time a site uses the element, the browser shows its own prompt. If the person agrees, the browser remembers that choice for that site and that age, and later checks on the same site skip the browser’s prompt. A site that asks about a different age gets a new prompt. A site should still remember a successful check in its own session rather than asking again. In private browsing, the browser forgets these choices when the private browsing session ends. People can remove these choices in the browser’s site settings.

Processing model

The element starts from a click or tap on it or its label, a key press, or an assistive technology’s press action. Which keys depends on its role, which is an open question. The browser requires a trusted event. A trusted event is one the browser creates from the person’s own input, not from script. The event must aim at the element or its label. So click() and events that script dispatches do nothing, even inside the person’s click on something else. HTML lets each platform decide what a click on a label does. This proposal sets a new rule. A click on the element’s label starts a check only if that click is a trusted event.

When the person starts the element, the browser takes a snapshot of age and uses it for the steps below, so the person answers the question they saw:

  1. If the element is disabled, if meetsAge is already true, or if a check from this element is already running, stop without firing an event.
  2. If the element isn’t in a secure-context, top-level document, for example inside an iframe or an embedded object, fire error and stop.
  3. If age isn’t 13, 16, or 18, fire error and stop. For this step and step 2, the browser also writes the reason to the developer console, which scripts can’t read.
  4. On every check, the browser asks the platform for the person’s age range. The browser lets the platform show a prompt if the platform chooses. The browser passes the ages 13, 16, and 18 to the platform.
    • Some platforms have an anniversary rule. For these platforms, the browser keeps no copy, so the platform’s own rules apply every time. These rules include a parent’s choice to ask the child each time.
    • A platform might lack an anniversary rule. For this platform, the browser keeps the answer for a user-agent specific time period (e.g., one year) and uses the answer for every site, so the answer still changes only on an anniversary.
    • The platform might stop sharing. The browser drops its copy, so a parent’s change applies at once.
    • For an age the browser stores itself, the rules in “Where the age comes from” apply.
    • If the person declines in the platform’s prompt, the browser fires error. The browser then stops.
    • For any other result, the browser goes to the next step. A failure counts as another result.
  5. The browser shows its own prompt, naming the site (for example, the origin). The browser skips the prompt only if the person already agreed to share this age with this site and the platform still says the person is at least that age.
    • If the browser stores the age itself and has no age yet, the prompt lets the person select the age group the person is in. The browser continues the steps with that declared age range.
    • The prompt says the age can’t be shared when any of these is true:
      • the platform refused, failed for any reason, or has no age for the person;
      • the person does not select an age group;
      • the range shows the person isn’t at least the age, or can’t say either way.
    • If the platform says the person turned sharing off, the prompt says where the person can change sharing. The prompt adds an example like the system settings.
    • When the platform has no age for the person, the prompt adds that no age is set. The prompt tells the person where to set an age.
    • If the platform fails because of a network problem, the prompt tells the person to try again later.
    • Otherwise, the prompt asks whether to share that the person is at least the age.
    • In every case above where the prompt says the age can’t be shared, when the person closes the prompt, the browser fires error and stops.
    • If the person cancels the prompt, the browser fires error and stops.
    • The prompt is modal. The person cannot use the page until the person answers the prompt.
  6. If the element is no longer in the document, if the document is no longer fully active, if age no longer matches the snapshot, if the element is now disabled, or if the form reset since the check started, fire error, write the reason to the developer console, and stop. Otherwise set meetsAge to true and fire change.

Once steps 1 to 3 pass, every failure that depends on the person’s choice or age reaches the page only after the person has spent time on a prompt. The browser adds no timing difference of its own, but how long the person takes can still differ (see “Human timing” under “Open risks”). The browser never asks someone to share an age they don’t meet.

With required, the element counts as missing a value until meetsAge is true.

When the form submits, if name is set and meetsAge is true, add one entry named name. Its value is the age it checked, such as 18. If meetsAge is false, add nothing, the same as an unticked checkbox.

When the form resets, or when age changes to a different value, clear meetsAge. The browser never restores meetsAge after navigation. When a page comes back from the back/forward cache, the browser clears meetsAge before the pageshow event, so the page can update. The browser never copies meetsAge when the element is cloned. When the person answers the prompt after a reset, the browser drops the answer and fires error.

The steps after the browser’s prompt appears run in parallel, so scripts on the page keep running while the person decides.

Automated testing

Like the Digital Credentials API’s WebDriver module, a WebDriver extension, available only in automation sessions, could set a test age and drive the prompt, so sites and browsers can test the element automatically.

To do: define that extension. It needs a command to set the test age, and a way to agree to or cancel the browser’s prompt.

Web IDL

[Exposed=Window, SecureContext]
interface HTMLAgeCheckElement : HTMLElement {
  [HTMLConstructor] constructor();

  [CEReactions] attribute unsigned long age;
  readonly attribute boolean meetsAge;

  readonly attribute HTMLFormElement? form;
  [CEReactions] attribute DOMString name;
  [CEReactions] attribute boolean disabled;
  [CEReactions] attribute boolean required;
  readonly attribute NodeList labels;
  readonly attribute boolean willValidate;
  readonly attribute ValidityState validity;
  readonly attribute DOMString validationMessage;
  boolean checkValidity();
  boolean reportValidity();
  undefined setCustomValidity(DOMString error);
};

Accessibility and internationalization

The browser gives the element its role, keyboard handling, and focus return. The element’s name comes from its visible text or its label, so voice control users can say what they see. When the prompt closes, focus returns to the element if it can take focus, and otherwise to where it was before.

The prompt belongs to the browser or platform, not the page, but its accessibility is in scope. It should follow WCAG 2.2, and where it isn’t web content, it should follow WCAG 2.2 as WCAG2ICT describes, the W3C guidance on applying WCAG to software that isn’t a web page. Its content is exposed to assistive technology with the right names, roles, and values. It doesn’t rely on a single sense or way of interacting, and it gives people enough time.

The prompts use the language of the browser’s or the platform’s own interface. They must show site names in any script, including right-to-left scripts, without letting a name pretend to be another site.

Privacy and security

Open questions

Stakeholder feedback

No public positions yet.

The <agecheck> element doesn’t replace these. Several solve different problems, and they can work alongside it.

Mozilla’s content labelling (whatwg/html#11722). Sites label adult pages, and the browser blocks them for people whose platform restricts adult content. The site learns nothing. It blocks whole pages, while the <agecheck> element lets a site check before showing something specific.

Mozilla’s Prefer: safe header (whatwg/fetch#1891). The browser tells every site the person prefers safe content, without the site asking. The <agecheck> element answers only a site that asks, after the person agrees.

An Age-Range header, sketched by Nick Doty (Center for Democracy & Technology). It sends the person’s range to every site that opts in, on every request, with no prompt per site. The <agecheck> element answers one age per check, after a prompt.

The Digital Credentials API, with or without passkeys. It can carry a cryptographic proof, but it needs a digital ID, which many people don’t have. It doesn’t guarantee that only the age is revealed, and it lets a site ask for more than age. Presenting an ID is also heavy for a simple age check. The <agecheck> element uses the age the platform already has, and asks only about age.

A JavaScript method could open the same prompt. But it would still need a user action to start it, and wouldn’t work in a form without script. For a canvas, the HTML-in-Canvas proposal would let the element itself be drawn inside the canvas, if a click on a drawn element counts as the person’s own click.

Open risks

User research

None of our own yet.