The HTML <permission> Element

Draft Community Group Report,

This version:
https://wicg.github.io/PEPC/permission-element.html
Issue Tracking:
GitHub
Inline In Spec
Editor:
Daniel Vogelheim (Google LLC)

Abstract

A <permission> HTML element to request browser permissions in-page.

Suitable styling and UI constraints on this new element ensure that the user understands what a click on it means, and thus gives the browser a high level of confidence of user intent to make a permission decision. The <permission> element aims to be more accessible and more secure than the current permission flows.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. Introduction

User agents expose powerful features to web sites, which are features that are important to some use cases, but can be easily abused. The arguably canonical example of such a powerful feature is camera access, which is essential to many use cases like online meetups, but unsolicited camera activation would be a major privacy issue. To handle this, user agents use permissions to ask the user whether they wish for a particular access to be allowed or not.

These permission requests began as a fairly direct passthrough: A site would ask for some capability and the user agent immediately prompts the user to make a decision for the request. Meanwhile, spam and abuse have forced user agents to take a more opinionated approach to protect users' security, privacy, and attention. The status quo is that users get a multitude of permission requests, where it’s oftentimes unclear to users what the consequences of these requests might be.

This spec introduces a new mechanism that requests access to powerful features through an in-page element, with built-in protections against abuse. This wants to tie permission requests to the actual context in which they will be used, thus reducing "permission spam" and at the same time providing implementations with a better signal of user intent.

2. The permission element.

Categories:
Flow content.
Phrasing content.
Interactive content.
Palpable content.
Contexts in which this element can be used:
Where phrasing content is expected.
Content model:
Nothing.
Content attributes:
Global attributes
type — Type of permission this element applies to.
isValid — query whether the element can currently be activated.
invalidReason — Return a string representation of why the element currently cannot be activated.
ondismiss — notifies when the user has dismissed the permission prompt.
onresolve — notifies when a permission prompt has been answered by the user (positively or negatively).
onvalidationstatuschange — notifies when the validation status changes.
Accessibility considerations:
DOM interface:
[Exposed=Window]
interface HTMLPermissionElement : HTMLElement {
  [HTMLConstructor] constructor();
  [CEReactions, Reflect] attribute DOMString type;

  readonly attribute boolean isValid;
  readonly attribute PermissionElementBlockerReason invalidReason;

  attribute EventHandler onresolve;
  attribute EventHandler ondismiss;
  attribute EventHandler onvalidationstatuschange;
};

Add accessibility considerations.

Check attribute & event handler & invalid reason names against current proposal(s).

The type attribute controls the behavior of the permission element when it is activated. Is is an enumerated attribute, whose values are the names of powerful features. It has neither a missing value default state nor a invalid value default state.

The isValid attribute reflects whether a the permission element is not currently blocked.

The invalidReason attribute is an enumerated attribute that reflects the internal state of the permission element. It’s value set are PermissionElementBlockerReason

The global lang attribute is observed by the permission element to select localized text.

The following are the event handlers (and their corresponding event handler event types) that must be supported on permission elements event handler IDL attributes:

onresolve Event
ondismiss Event
onvalidationstatuschange Event

onvalidationstatuschange is probably not a simple Event.

2.1. permission element internal state

The permission element represents a user-requestable permission, which the user can activate to enable (or disable) a particular permission or set of permissions. It is core to the permission element that these requests are triggered by the user, and not by the page’s script. To enforce this, the element checks whether the activation event is trusted. Additionally it watches a number of conditions, like whether the element is (partially) occluded, or if it has recently been moved. The element maintains an internal [[BlockerList]] to keep track of this.

The permission element has the following internal slots:

2.2. permission-supporting state at the navigable

In order to support the permission element, the navigable maintains an ordered set of permission elements, [[PermissionElements]]. This ordered set is used to evaluate the blockers of type unsuccesful_registration.

2.3. permission element interesting behaviours

The permission element has a few surprising behaviours, to support its security properties:

2.3.1. The type property

The permission type cannot be modified. Modifying the permission type at will may lead to user confusion, and hence we’d like to prevent it. Since, however, a page may create a permission element dynamically we still need to offer an API to modify it. To do do, we distinguish between a freshly initialized and an empty or invalid (no permission) state, where the former allows setting the type and the latter does not.

Example:

// Changing a valid type:
var pepc = document.createElement("permission");
pepc.type = "camera";  // Okay.
pepc.type;  // "camera".
pepc.type = "geolocation";  // Not okay. Would have been okay as initial assignment.
pepc.type;  // "camera". Reflects the internal state, which has not changed.

// Setting an invalid type:
pepc = document.createElement("permission");
pepc.type = "icecream";  // Ice cream is not a powerful browser feature. Not okay.
pepc.type;  // "". Reflects the internal state.
pepc.type = "camera";  // Still Not okay, because type as already been set.
                       // Would have been okay as initial assignment.
pepc.type;  // "". Reflects the internal state, which has not changed.

The HTMLPermissionElement’s type getter steps are:
  1. If [[Types]] is null: Return "".

  2. Return a string, containing the concatenation of all powerful feature names in [[Types]], seperated by " ".

The HTMLPermissionElement’s type setter steps are:
  1. If [[Types]] is not null: Return.

  2. Set [[Types]] to «[]».

  3. Parse the input as a string of powerful feature names, seperated by whitespace.

  4. If any errors occured, return.

  5. Check if the set of powerful features is supported for the HTMLPermissionElement by the user agent. If not, return.

  6. Append each powerful feature name to the [[Types]] ordered set.

Note: The supported sets of powerful features is implementation-defined.

2.3.2. Activation blockers

The key goal of the permission element is to reflect a user’s conscious choice, and we need to make sure the user cannot easily be tricked into activating it. To do so, the permission maintains a list of blocker reasons, which may - permanently or temporarily - prevent the element from being activated.

enum PermissionElementBlockerReason {
  "",  // No blocker reason.
  "type_invalid", "illegal_subframe", "unsuccesful_registration",
  "recently_attached", "intersection_changed",
  "intersection_out_of_viewport_or_clipped",
  "intersection_occluded_or_distorted", "style_invalid"
};

The permission element keeps track of "blockers", reasons why the element (currently) cannot be activated. These blockers come with three lifetimes: Permanent, temporary, and expiring.

Permanent blocker

Once an element has a permanent blocker, it will be disabled permanently. There are used for issues that the website owner is expected to fix. An example is a permission element inside a fencedframe.

Temporary blocker

This is a blocker that will only be valid until the blocking condition no no longer occurs. An example is a permission element that is not currently in view. All temporary blockers turn into expiring blockers once the condition no longer applies.

Expiring blocker

This is a blocker that is only valid for a fixed period of time. This is used to block abuse scenarios like "click jacking". An example is a permission element that has recently been moved.

Blocker name Blocker type Example condition Order hint
type_invalid permanent When an unsupported permission type has been set. 1
illegal_subframe permanent When the permission element is used inside a fencedframe. 2
unsuccesful_registration temporary When too many other permission elements for the same powerful feature have been inserted into the same document. 3
recently_attached expiring When the permission element has just been attached to the DOM. 4
intersection_changed expiring When the permission element is being moved. 6
intersection_out_of_viewport_or_clipped temporary When the permission element is not or not fully in the viewport. 7
intersection_occluded_or_distorted temporary When the permission element is fully in the viewport, but still not fully visible (e.g. because it’s partly behind other content). 8
style_invalid temporary 9
To add a blocker with a PermissionElementBlockerReason reason and an optional flag expires:
  1. Assert: reason is not "". (The empty string in PermissionElementBlockerReason signals no blocker is present. Why would you add a non-blocking blockern empty string?)

  2. Let timestamp be None.

  3. If expires, then let timestamp be current high resolution time plus the blocker delay.

  4. Append an entry to the internal [[BlockerList]] with reason and timestamp.

The blocker delay is 500ms.
To add an expiring blocker with a PermissionElementBlockerReason reason:
  1. Assert: reason is listed as "expiring" in the blocker reason table.

  2. Add a blocker with reason and true.

To add a temporary blocker with a PermissionElementBlockerReason reason:
  1. Assert: reason is listed as "temporary" in the blocker reason table.

  2. Add a blocker with reason and false.

To add a permanent blocker with a PermissionElementBlockerReason reason:
  1. Assert: reason is listed as "permanent" in the blocker reason table.

  2. Add a blocker with reason and false.

To remove blockers with PermissionElementBlockerReason reason from an element:
  1. Assert: reason is listed as "temporary" in the blocker reason table.

  2. For each entry in element’s [[BlockerList]]:

    1. If entry’s reason equals reason, then remove entry from element’s [[BlockerList]].

  3. Add a blocker with reason and true.

To determine a HTMLPermissionElement element’s blocker:
  1. Let blockers be the result of sorting element’s [[BlockerList]] with the blocker ordering algorithm.

  2. If blockers is not empty and blockers[0] is blocking, then return blockers[0].

  3. Return nothing.

To determine blocker ordering for two blockers a and b:
  1. Let really large number be 99.

  2. Assert: No order hint in the blocker reason table is equal to or greater than really large number.

  3. If a is blocking, then let a hint be the order hint of a’s reason in the blocker reason table, otherwise let a hint be really large number.

  4. If b is blocking, then let b hint be the order hint of b’s reason in the blocker reason table, otherwise let b hint be really large number.

  5. Return whether a hint is less than or equal to b hint.

An HTMLPermissionElement's blocker list’s entry is blocking if:
  1. entry has no blocker timestamp,

  2. or entry has a blocker timestamp, and the blocker timestamp is greater or equal to the current high resolution time.

NOTE: The spec maintains blockers as a list [[BlockerList]], which may potentially grow indefinitely (since some blocker types simply expire, but are not removed). This structure is chosen for the simplicity of explanation, rather than for efficiency. The details of this blocker structure are not observable except for a handful of algorithms defined here, which should open plenty of opportunities for implementations to handle this more efficiently.

2.4. permission element algorithms

The HTMLPermissionElement constructor steps are:
  1. Initialize the internal [[Types]] slot to null.

  2. Initialize the internal [[BlockerList]] to «[]».

The HTMLPermissionElement insertion steps are:
  1. If [[Types]] is null, set [[Types]] to «[]».

  2. Initialize the internal [[BlockerList]] to «[]».

  3. Append this to node navigable's [[PermissionElements]].

  4. Initialize the internal [[IntersectionRect]] with undefined.

  5. Initialize the internal [[IntersectionObserver]] with the result of constructing a new IntersectionObserver, with IntersectionObserver callback.

  6. Call [[IntersectionObserver]].observe(this).

  7. If [[Types]] is empty, then add a permanent blocker with reason type_invalid.

  8. If this is not type permissible, then add a temporary blocker with unsuccesful_registration.

  9. Add an expiring blocker with reason recently_attached.

  10. If the traversable navigable of the node navigable of this is a fenced navigable, then add a permanent blocker with illegal_subframe.

The HTMLPermissionElement removing steps are:
  1. Remove this from node navigable's [[PermissionElements]].

  2. Recheck type permissibility for this's node navigable.

HTMLPermissionElement element’s isValid getter steps are:
  1. Return whether element’s blocker is Nothing.

HTMLPermissionElement element’s invalidReason getter steps are:
  1. If element’s blocker is Nothing, return "".

  2. Otherwise, element’s blocker's reason string.

A permission element’s activation behavior given event is:
  1. Assert: element’s [[Types]] is not null.

  2. If element’s [[Types]] is empty, then return.

  3. If event.isTrusted is false, then return.

  4. If element.isValid is false, then return.

  5. Request permission to use the powerful features named in element’s [[Types]].

What about event handlers?

The HTMLPermissionElement’s IntersectionObserver callback implements IntersectionObserverCallback and runs the following steps:
  1. Assert: The IntersectionObserver's root is the Document

  2. Let entries be the value of the first callback parameter, the list of intersection observer entries.

  3. Assert: entries is not empty.

  4. Let entry be entries’s last item.

  5. If entry.isVisible, then:

    1. Remove blockers with intersection_occluded_or_distorted.

    2. Remove blockers with intersection_out_of_viewport_or_clipped.

  6. Otherwise:

    1. If entry.intersectionRatio >= 1, then:

      1. Let reason be intersection_occluded_or_distorted.

    2. Otherwise:

      1. Let reason be intersection_out_of_viewport_or_clipped.

    3. Add a temporary blocker with reason.

  7. If [[IntersectionRect]] does not equal entry.intersectionRect then add an expiring blocker with intersection_changed.

  8. Set [[IntersectionRect]] to entry.intersectionRect

Do I need to define dictionary equality?

To determine whether an element is type permissible:
  1. Assert: element’s node navigable's [[PermissionElements]] contains element.

  2. Let count be 0.

  3. For each current in element’s node navigable's [[PermissionElements]]:

    1. If current is element, then break.

    2. If the intersection of element.[[Types]] with current.[[Types]] is not empty, then increment count by 1.

  4. Return whether count is less than 2.

To recheck type permissibility for a document:
  1. For each current in document’s [[PermissionElements]]:

    1. If current is type permissible, then remove blockers with unsuccesful_registration from current.

3. Rendering the permission Element

3.1. Presentation

The permission element is a devolvable widget and is chiefly rendered like a button. The button label is largely expected to be determined by the browser, rather than the page, and reflects the powerful features listed in [[Types]], expressed as text and icons.

The page can influence the permission element’s styling, but with constraints to prevent abuse (e.g. minimum and maximum sizes for fonts and the label itself). The page can also select a locale for the text via the lang attribute.

3.2. Styling

A permission element is expected to render with the following styles:

@namespace "http://www.w3.org/1999/xhtml";
permission {
  opacity: 1.0;
  line-height: normal;
  whitespace: nowrap;
  user-select: none;
  appearance: auto;

  outline-offset: clamp(0, inherit, none); /* No negative outline-offsets. */
  font-weight: clamp(200, inherit, none);  /* No font-weights below 200. */
  word-spacing: clamp(0, inerhit, 0.5em);  /* Word-spacing between 0..0.5em */
  letter-spacing: clamp(-0.05em, inherit, 0.2em);  /* Letter spacing between -0.05..0.2em */

  /*
   * Below, we assume that, if no value is specified, this will clamp to the
   * minimum value, thus effectively providing a default, too.
   */
  min-height: clamp(1em, inherit, none);
  max-height: clamp(none, inherit, 3em);
}

Additionally, a permission element puts contraints on a number of additional styles. If one of these conditions is not met, then a temporary blocker is added with type style_invalid.

color, background-color Set by default to the user agent’s default button colors. The contrast ratio between the 2 colors needs to be at least 3. Alpha has to be 1.
font-size Must be set larger or equal than small and less than or equal to xx-large.

font-size: Explainer says, "Zoom will be taken into account when computing font-size." I have no idea what that means with absolute font sizes.

The following conditions on the style are resolved by modifying the style:

margin, and any margin-* properties Values under 4px will be corrected to 4px. This is done to help prevent false positives for the logic that detects the element being covered by something else.
font-style Defaults to normal. If values other than normal or italic are specified, the default will be used instead.
display Defaults to inline-block. If values other than inline-block or none are specified, the default will be used again.
min-width Defaults to fit-content. If a value exists, the <max()> of fit-content and the computed value is used.
max-width Defaults to 3 * fit-content. If a value exists, <min()> between the default and the inherited value will be used. However this does not apply if the element has a border with a width of at least 1px and a color that has a contrast ratio with the background-color of at least 3 and alpha of 1.
padding-top If height is auto, use padding-top: clamp(1em, inherit, none); padding-bottom: calc(padding-top); Otherwise, ignore.
padding-left If width is auto, use padding-left: clamp(none, inherit, 5em); padding-right: calc(padding-left); Otherwise, ...

Why the complicated max-width rule?

padding-left description said: "Otherwise, This does not apply under the same border conditions as max-width, except padding-right with still be set to the value of padding-left." I can’t parse that.

4. Security & Privacy Considerations

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CSS-DISPLAY-4]
CSS Display Module Level 4. Editor's Draft. URL: https://drafts.csswg.org/css-display-4/
[CSS-FONTS-4]
Chris Lilley. CSS Fonts Module Level 4. URL: https://drafts.csswg.org/css-fonts-4/
[CSS-SIZING-3]
Tab Atkins Jr.; Elika Etemad. CSS Box Sizing Module Level 3. URL: https://drafts.csswg.org/css-sizing-3/
[CSS-SIZING-4]
Tab Atkins Jr.; Elika Etemad; Jen Simmons. CSS Box Sizing Module Level 4. URL: https://drafts.csswg.org/css-sizing-4/
[CSS-UI-4]
Florian Rivoal. CSS Basic User Interface Module Level 4. URL: https://drafts.csswg.org/css-ui-4/
[CSS-VALUES-4]
Tab Atkins Jr.; Elika Etemad. CSS Values and Units Module Level 4. URL: https://drafts.csswg.org/css-values-4/
[CSS21]
Bert Bos; et al. Cascading Style Sheets Level 2 Revision 1 (CSS 2.1) Specification. URL: https://drafts.csswg.org/css2/
[CSS22]
Bert Bos. Cascading Style Sheets Level 2 Revision 2 (CSS 2.2) Specification. URL: https://drafts.csswg.org/css2/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[FENCED-FRAME]
Fenced Frame. Draft Community Group Report. URL: https://wicg.github.io/fenced-frame/
[GEOMETRY-1]
Simon Pieters; Chris Harrelson. Geometry Interfaces Module Level 1. URL: https://drafts.fxtf.org/geometry/
[HR-TIME-3]
Yoav Weiss. High Resolution Time. URL: https://w3c.github.io/hr-time/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[INTERSECTION-OBSERVER]
Stefan Zager; Emilio Cobos Álvarez; Traian Captan. Intersection Observer. URL: https://w3c.github.io/IntersectionObserver/
[MANIFEST-APP-INFO]
Aaron Gustafson. Web App Manifest - Application Information. URL: https://w3c.github.io/manifest-app-info/
[MEDIAQUERIES-5]
Dean Jackson; et al. Media Queries Level 5. URL: https://drafts.csswg.org/mediaqueries-5/
[PERMISSIONS]
Marcos Caceres; Mike Taylor. Permissions. URL: https://w3c.github.io/permissions/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

IDL Index

[Exposed=Window]
interface HTMLPermissionElement : HTMLElement {
  [HTMLConstructor] constructor();
  [CEReactions, Reflect] attribute DOMString type;

  readonly attribute boolean isValid;
  readonly attribute PermissionElementBlockerReason invalidReason;

  attribute EventHandler onresolve;
  attribute EventHandler ondismiss;
  attribute EventHandler onvalidationstatuschange;
};

enum PermissionElementBlockerReason {
  "",  // No blocker reason.
  "type_invalid", "illegal_subframe", "unsuccesful_registration",
  "recently_attached", "intersection_changed",
  "intersection_out_of_viewport_or_clipped",
  "intersection_occluded_or_distorted", "style_invalid"
};

Issues Index

Add accessibility considerations.
Check attribute & event handler & invalid reason names against current proposal(s).
onvalidationstatuschange is probably not a simple Event.
What about event handlers?
Do I need to define dictionary equality?
font-size: Explainer says, "Zoom will be taken into account when computing font-size." I have no idea what that means with absolute font sizes.
Why the complicated max-width rule?
padding-left description said: "Otherwise, This does not apply under the same border conditions as max-width, except padding-right with still be set to the value of padding-left." I can’t parse that.