1. Principles behind design of Web APIs
1.1. Put user needs first (Priority of Constituencies)
If a trade-off needs to be made, always put user needs above all.
The internet is for end users : any change made to the web platform has the potential to affect vast numbers of people, and may have a profound impact on any person’s life.
User needs come before the needs of web page authors, which come before than the needs of user agent implementors, which come before than the needs of specification writers, which come before theoretical purity.
Similarly, when beginning to design an API, be sure to understand and document the user need that the API aims to address.
Like all principles, this is not absolute. Ease of authoring affects how content reaches users. User agents have to prioritize finite engineering resources, which affects how features reach authors. Specification writers also have finite resources, and theoretical concerns reflect underlying needs of all of these groups.
See also:
1.2. It should be safe to visit a web page
Hyperlinks, links from one page to another, are one of the foundations of the Web.
Following a link, or visiting a web page, should be safe: users doing this should not have to fear for the security of their computer or for essential aspects of their privacy. (But it’s not completely safe, in the sense that users may face consequences if their use of the Web is harming others.) Furthermore, users should understand that it is safe (and how it isn’t) so they can make informed decisions between use of the Web versus other technologies.
Saying “essential aspects” here skips over quite a bit of detail. The Web today is far from being perfectly private. One aspect of privacy problems is when reality doesn’t meet expectations. For example, a person walking down the street generally expects to be recognized by their friends, but (depending on the country) may not expect that they walked down that street at that time to be recorded in a permanent government database. Online, people might have less understanding of what is or isn’t possible, and their expectations might differ more widely.
We can probably make privacy on the Web better than it is today:
-
We can improve the user interfaces through which the Web is used to make it clearer what users of the Web should expect.
-
We can change the technical foundations of the Web so that they match user expectations of privacy.
-
We can consider the cases where users would be better off if expectations were higher, and in those cases try to change both technical foundations and expectations
When we add new features to the Web that might weaken the security or privacy characteristics that the Web currently has, we should consider the tradeoffs involved in that particular feature. A new feature with security risks might still make users safer, for example, because they would download and run native binary software less often, and downloading and running binaries is much riskier than visiting a Web page. However, we should also consider the tradeoff against the idea that it is safe to visit Web pages: adding a risky feature to the Web teaches the Web’s users that the Web is less safe, and this erodes one of the core values of the Web: that it is safe to visit websites. We should seek to give users a true picture of the safety of the Web so that they can act based on that understanding of safety (in comparison to the safety of alternatives), and at the same time we should seek to keep the Web safe for its users and work to make it safer.
1.3. Trusted user interface should be trustworthy
Since users of the Web can follow links to sites they might not know already, software that lets them access the Web generally provides user interface to show the user who they’re interacting with and how, such as by displaying part or all of the URL or other information about the site, or whether the connection is secure. Since users rely on this information to learn what site they’re on and to make judgments about whether it is trustworthy, this software is designed so that sites are not able to override this user interface or spoof it in a way that would confuse users. Therefore, when we add new features to the web, we should consider whether they require new complexity in this user interface, and whether that complexity would reduce users' ability to make correct judgments about who they’re interacting with or whether the user interface is trustworthy or spoofed.
1.4. Ask users for meaningful consent when appropriate
In some cases, we might add features to the Web that are not appropriate to allow for any website a user visits, but that are reasonable to have if the user agrees to use the feature based on a reasonable understanding of what will happen as a result. One example of such a feature might be support for location: many users seem likely to understand what it means to share their current location with a website and be able to consent to doing so (even though they might not fully understand the privacy implications of doing so). Another example of such a feature would be camera access, where users understand that they’re sharing the image in the rectangle visible to the camera (even if they might not understand everything someone might do with the data).
We should not depend on asking the user for consent (via a permission prompt or other mechanism) if we don’t have a way to express that request in a way that users will understand what is being requested and the main implications of giving their consent. Understanding whether this is the case may require studying users understanding of potential user interfaces, although in many cases experienced user experience designers may be able to extrapolate from previous experience.
We should also not ask users to consent to something that can also happen if the user does not consent. The existence of prompts that request a user’s consent for something (from trusted user interface in their browser or other user agent) imply that the thing for which consent is being requested will not happen if the user doesn’t consent. Therefore we should not have user agents request consent for something that they can’t effectively prevent. Doing so would send the wrong message about the safety characteristics of the Web and would lead users to be disappointed when the expectations of privacy, which they learned from trusted user interface, did not match reality.
Asking users for consent via permission prompts can reinforce the idea that the web is safe by showing the user that certain things won’t happen without their permission. But frequently asking users for consent can also show how scary a place the web is by showing how many sites are willing to ask for intrusive and unnecessary permissions.
1.5. Support the full range of devices and platforms
Features on the web should work across different input and output devices, different platforms, and different media, as much as this is reasonably possible. For example, they should not be specific to a particular screen size, a particular operating system or browser engine, or assume the user is using a mouse.
It is not always possible for a feature to work in all contexts: for example, hyperlinks don’t work when printed on paper, and :hover doesn’t work on touch input devices where there isn’t a mouse pointer position. However, these features are still valuable across a range of devices, and can be adapted to devices that don’t support their original intent (for example, hyperlink targets may be converted to footnotes when printing).
Features
should
also
be
designed
so
that
the
easiest
way
to
use
them
works
across
devices
as
well.
For
example,
the
display:block
,
display:
flex
,
and
display:
grid
layout
models
in
CSS
all
default
to
placing
content
within
the
available
space
and
without
overlap,
so
that
it
works
across
devices
of
various
sizes,
across
browsers
and
operating
systems
that
have
different
fonts
available,
and
across
different
user
settings
for
default
text
size.
2. API Design Across Languages
2.1. Prefer simple solutions
Simple solutions are generally better than complex solutions, although they may be harder to find. Simpler features are easier for user agents to implement and test, more likely to be interoperable, and easier for authors to understand. See related [LEAST-POWER]
2.2. Naming things
Names take meaning from:
-
signposting (the name itself)
-
use (how people come to understand the name over time)
-
context (the object on the left-hand side, for example)
Use common words
API naming must be done in easily readable US English. Keep in mind that most web developers are not native English speakers. Whenever possible, names should be chosen that use common vocabulary non-native English speakers are likely to understand when first encountering the name.
Value readability over brevity, though always keep in mind that sometimes the shorter name is the clearer one. For instance, it may be appropriate to use technical language or well-known terms of art in the specification where the API is defined.
For
example,
the
Fetch
API’s
Body
mixin’s
json()
method
is
named
for
the
kind
of
object
it
returns.
JSON
is
a
well-known
term
of
art
among
web
developers
likely
to
use
the
Fetch
API.
It
would
harm
comprehension
to
name
this
API
less
directly
connected
to
its
return
type.
[FETCH]
Use ASCII names
Names must adhere to the local language restrictions, for example CSS ident rules etc. and should be in the ASCII range .
Consultation
Please consult widely on names in your APIs. You may find good names or inspiration in surprising places. What are similar APIs named on other platforms, or in popular libraries in various programming languages? Ask end users and developers what they call things that your API works with or manipulates. Look at other web platform specifications, and seek advice from others working in related areas of the platform.
Pay particular attention to advice you receive with clearly-stated rationale based on underlying principles.
Tantek Çelik extensively researched how to name the various pieces of a URL. The editors of the URL spec have relied on this research when editing that document. [URL]
Future-proofing
Naming should be generic and future-proof whenever possible.
The name should not be directly associated with a brand or specific revision of the underlying technology whenever possible; technology becomes obsolete, and removing APIs from the web is difficult.
The Remote Playback API was not named after one of the pre-existing, proprietary systems it was inspired by (such as Chromecast or AirPlay). Instead, general terms that describe what the API does were chosen. [REMOTE-PLAYBACK]
The
keydown
and
keyup
KeyboardEvent
s
were
not
named
for
the
specific
hardware
bus
that
keyboards
used
at
the
time.
Instead,
generic
names
were
chosen
that
are
as
applicable
to
today’s
Bluetooth
and
USB
keyboards
as
they
were
to
PS/2
and
ADB
keyboards
back
then.
[UIEVENTS]
Consistency
Naming schemes should aim for consistency, to avoid confusion.Sets of related names should agree with each other in:
-
part of speech - noun, verb, etc.
-
negation, for example all of the names in a set should either describe what is allowed or they should all describe what is denied
Boolean properties vs. boolean-returning methods
Boolean
properties,
options,
or
API
parameters
which
are
asking
a
question
about
their
argument
should
not
be
prefixed
with
is
,
while
methods
that
serve
the
same
purpose,
given
that
it
has
no
side
effects,
should
be
prefixed
with
is
to
be
consistent
with
the
rest
of
the
platform.
Use casing rules consistent with existing APIs
Although they haven’t always been uniformly followed, through the history of web platform API design, the following rules have emerged:
Casing rule | Examples | |
---|---|---|
Methods
and
properties
(Web IDL attributes, operations, and dictionary keys) | Camel case |
document
document
|
Classes
and
mixins
(Web IDL interfaces) | Pascal case |
NamedNodeMap
NonElementParentNode
|
Initialisms in APIs | All caps, except when the first word in a method or property |
HTMLCollection
element
document
|
Repeated initialisms in APIs | Follow the same rule |
HTMLHRElement
RTCDTMFSender
|
The abbreviation of "identity" |
Id
,
except
when
the
first
word
in
a
method
or
property
|
node
event
credential
|
Enumeration values | Lowercase, dash-delimited |
|
Events | Lowercase, concatenated |
autocompleteerror
languagechange
|
HTML elements and attributes | Lowercase, concatenated |
|
JSON keys | Lowercase, underscore-delimited |
manifest
|
ismap
on
img
elements
is
reflected
as
the
isMap
property
on
HTMLImageElement
.
The rules for JSON keys are meant to apply to specific JSON file formats sent over HTTP or stored on disk, and don’t apply to the general notion of JavaScript object keys.
Repeated
initialisms
are
particularly
non-uniform
throughout
the
platform.
Infamous
historical
examples
that
violate
the
above
rules
are
XMLHttpRequest
and
HTMLHtmlElement
.
Do
not
follow
their
example;
instead
always
capitalize
your
initialisms,
even
if
they
are
repeated.
Warning about dangerous features
Where possible, mark features that weaken the guarantees provided to developers by making their names start with "unsafe" so that this is more noticeable.
For
example,
Content
Security
Policy
(CSP)
provides
protection
against
certain
types
of
content
injection
vulnerabilities.
CSP
also
provides
features
that
weaken
this
guarantee,
such
as
the
unsafe-inline
keyword,
which
reduces
CSP’s
own
protections
by
allowing
inline
scripts.
2.3. New features should be detectable
The existence of new features should generally be detectable, so that web content can act appropriately whether the feature is present or not. This applies both to features that are not present because they are not implemented, and to features that may not be present in a particular configuration (ranging from features that are present only on particular platforms to features that are only available in secure contexts).
It should generally be indistinguishable why a feature is unavailable, so that feature detection code written for one case of unavailability (e.g., the feature not being implemented yet in some browsers) also works in other cases (e.g., a library being used in a non-secure context when the feature is limited to secure contexts).
However, some features may be for interaction with devices that may or may not be available. Detection of device availability should generally be separate from feature detection. This helps developers who may want to handle device unavailability differently from lack of support for the feature. It also protects users; for more detail see § 8.2 Use care when exposing APIs for selecting or enumerating devices .
Detection should always be possible from script, but in some cases the feature should also be detectable in the language where it is used (such as @supports in CSS).
Note that some features should not be detectable. In general, features which add capabilities that sites can use should be detectable, whereas features which are for end users of the web rather than for developers should not be detectable. For instance, sites should not be able to detect if private browsing mode is engaged, or if assistive technologies are being used. See § 2.6 Do not reveal that private browsing mode is engaged , § 2.7 Do not reveal that assistive technologies are being used , and the Observations on Private Browsing Modes TAG Finding.
2.4. Consider limiting new features to secure contexts
It may save you significant time and effort to pre-emptively restrict your feature to Secure Contexts.
The TAG is on the record in supporting an industry-wide move to Secure the Web and applaud efforts to shift web traffic to secure connections. A great deal of effort has gone into debating which features should be restricted to Secure Contexts . Opinions vary amongst engine vendors, leading to difficult choices for feature designers. Some vendors require all new features be restricted this way, whereas others take a more selective approach.
This backdrop makes it difficult to provide advice about the extent to which your feature should be restricted. What we can highlight is that Secure Context-restricted features face the least friction in gaining wide adoption amongst these varying regimes.
Specification
authors
can
limit
most
features
defined
in
Web
IDL
,
to
secure
contexts
by
using
the
[
SecureContext
]
extended
attribute
on
interfaces,
namespaces,
or
their
members
(such
as
methods
and
attributes).
Similar
ways
of
marking
features
as
limited
to
secure
contexts
should
be
added
to
other
major
points
where
the
Web
platform
is
extended
over
time
(for
example,
the
definition
of
a
new
CSS
property).
However,
for
some
types
of
extension
points
(e.g.,
dispatching
an
event),
limitation
to
secure
contexts
should
just
be
defined
in
normative
prose
in
the
specification.
As described in § 2.3 New features should be detectable , the existence of features should generally be detectable, so that web content can act appropriately if the feature is present or not. Since the detection should be the same no matter why the feature is unavailable, a feature that is limited to secure contexts should, in non-secure contexts, be indistinguishable from a feature that is not implemented. However, if, for some reason (a reason that itself requires serious justification), it is not possible for developers to detect whether a feature is present, limiting the feature to secure contexts might cause problems for libraries that may be used in either secure or non-secure contexts.
If a feature would pose a risk to user privacy or security without the authentication, integrity, or confidentiality that is present only in secure contexts, then the feature must be limited to secure contexts. One example of a feature that should be limited to secure contexts is geolocation, since the authentication and confidentiality provided by secure contexts reduce the risks to user privacy.
2.5. Consider how your API should behave in private browsing mode
In general, APIs should behave the same in and out of private browsing mode . This is because the use of private browsing mode should not be revealed to sites. See § 2.6 Do not reveal that private browsing mode is engaged .
However, there are some situations in which APIs should consider behaving differently when private browsing mode is engaged, as discussed in our privacy & security questionnaire .
For instance, if your API would reveal information that would allow for the correlation of a single user’s activity both in and out of private browsing mode, consider possible mitigations such as introducing noise or augmenting permission prompts with additional information which would help users meaningfully consent to such correlation (see § 1.4 Ask users for meaningful consent when appropriate ).
The
Payment
Request
API
's
show()
method
,
when
called,
allows
User
Agents
to
act
as
if
the
user
had
immediately
aborted
the
payment
request
.
This enables User Agents to automatically abort payment requests in private browsing mode (thus protecting sensitive information such as the user’s shipping or billing address) without revealing that private browsing mode is engaged.
Private browsing modes enable users to browse the web without leaving any trace of their private browsing on their device. Therefore, APIs which provide client-side storage should not persist data stored while private browsing mode is engaged after it is disengaged. This can and should be done without revealing any detectable API differences to the site.
User Agents which support localStorage should not persist storage area changes made while private browsing mode is engaged.
If the User Agent has two simultaneous sessions with a site, one in private browsing mode and one not, storage area changes made in the private browsing mode session should not be revealed to the other browsing session, and vice versa. (The storage event should not be fired at the other session’s window object .)
See also
2.6. Do not reveal that private browsing mode is engaged
Some people use private browsing mode for their own safety ; just the fact that they have or are using private browsing mode may be sensitive information about them. Consider the harm that could be visited upon people—especially vulnerable people—were their use of private browsing mode revealed to others who wield power over them (such as employers, parents, partners, or state actors ).
Given such dangers, websites should not be able to detect that private browsing mode is engaged.
User Agents which support IndexedDB should not disable it in private browsing mode, because that would reveal that private browsing mode is engaged
See also
2.7. Do not reveal that assistive technologies are being used
The web platform must be accessible to people with disabilities. APIs which reveal the use of assistive technologies to sites enable sites to deny access to services to those who make use of assistive technologies.
People who make use of assistive technologies are often vulnerable members of society ; their use of assistive technologies is sensitive information about them. It should be safe for them to browse the web . Consider the risks of revealing their use of assistive technologies to others (including state actors ) who may wish them harm.
Given these two concerns, websites should not be able to detect that assitive technologies are being used.
The Accessibility Object Model (AOM) used to define a set of events which, when fired, revealed the use of assistive technology .
AOM has since removed these events and replaced them with synthetic DOM events which do not reveal the use of assistive technology.
See also
3. Cascading Style Sheets (CSS)
3.1. Separate CSS properties based on what should cascade separately
Decisions about what should be in the same CSS property or in different CSS properties should be based on what makes sense to set independently. CSS cascading allows declarations (which are property-value pairs) from different rules or different style sheets to override one another in a process called the cascade. Thus things that should always be set together belong in the same properties, but things that make sense to set in different places should be in separate properties.
For example, the "size" and "sink" aspects of the initial-letter property belong in a single property because they are part of a single initial letter effect (e.g., a drop cap, sunken cap, or raised cap) and should nearly always be specified together. However, the initial-letter-align property should be separate because it sets an alignment policy for all of these effects across the document which is a general stylistic choice and a function of the script (e.g., Latin, Cyrillic, Arabic) used in the document.
3.2. Make appropriate choices for whether CSS properties are inherited
An important choice in the design of every CSS property is whether the property is inherited . This affects what happens when no value is specified for the property on an element. If the property is not inherited, then that element gets the property’s initial value. If the property is inherited, then that element gets the parent element’s computed value.
The decision about whether a property should be inherited should be based largely on what should happen when the property is specified on multiple elements. If setting the property on a descendant element needs to override (rather than add to) the effect of setting it on an ancestor, then the property should probably be inherited. If setting the property on a descendant element is a separate effect that adds to setting it on an ancestor, then the property should probably not be inherited.
One specific example that is common is that if a property has an effect on text, then it is almost always in the class where a descendant element needs to override (rather than add to) the effect of setting it on an ancestor, and the property should be inherited. This is also needed to maintain the design principle that inserting an unstyled inline element around a piece of text does not change the appearance of that text.
A specification of an non-inherited property requiring that the handling of an element look at the value of that property on its ancestors (which may also be slow) is a "code smell" that suggests that the property likely should have been inherited.
Likewise, a specification of an inherited property requiring that the handling of an element ignore the value of a property if it’s the same as the value on the parent element is a "code smell" that suggests that the property likely should not have been inherited.
If the background-image property had been inherited, then the specification would have had to create a good bit of complexity to avoid a partially-transparent image being visibly repeated for each descendant element. This complexity probably would have involved behaving differently if the property had the same value on the parent element, which is the "code smell" mentioned above that suggests that a property likely should not have been inherited.
If the font-size property were not inherited, then it would probably have to have an initial value that requires walking up the ancestor chain to find the nearest ancestor that doesn’t have that value. This is the "code smell" mentioned above that suggests that a property likely should have been inherited.
As an inherited property, text-decoration would draw underlines (or other decorations) in the style and position of each piece of text. As a non-inherited property, it would draw underlines in the style and position of the element that specifies text-decoration . These behaviors lead to different results when the element that has text-decoration declared has descendants that are at a different vertical-alignment (e.g., superscripts), in a different color, or in a different font size.
The property was designed as not inherited because the behavior of having underlines have a consistent color, thickness, and position was considered the better behavior.
However, in the end, some of the characteristics of the other behavior (such as its interaction with visibility or text selection) were also seen as desirable, and it ended up being specified in a somewhat hybrid way that does involve the "code smell" of walking up the ancestor chain looking for text decorations on ancestors.
3.3. Choose the computed value type based on how the property should inherit
Another important choice in the design of every CSS property is what its computed value is. This is an abstract description of the value that is inherited when the property inherits. It is also important for starting and animation of CSS Transitions . This is most important for inherited properties, but it does matter for all properties.
Note:
Due
to
a
somewhat
unfortunate
history,
this
is
not
the
value
that
is
returned
by
getComputedStyle()
,
which
is
actually
the
resolved
value
,
which
can
be
the
result
of
further
computation
starting
from
the
computed
value.
The definition of a CSS property often has interactions with other CSS properties. It is often possible for these interactions to happen at the computed value stage or at a later stage such as the used value . Since inheritance operates on the computed value, whether an interaction should affect the value that descendants inherit is a major factor in the choice of where that interaction should be handled.
The dependencies that do affect computed values are required to not be circular.
Making decisions about whether dependencies should affect computed values often involves tradeoffs of complexity. Sometimes it is less complex for the behavior to happen as part of the computation of the computed value; sometimes it is less complex for the behavior to happen in whatever handles the computed value. (This complexity might be in the specification, in implementations, or both. Complexity in implementations might affect how fast things run for end users.)
For more information about computed values, see Computed Values Patterns .
However, the computed value in this case is the number 1.4 , not the length 28px . This is important because the value can inherit to elements that have a different font size, and preserving the "multiple of the current element’s font" behavior would be broken if it turned into a length at computed-value time:
< body style = "line-height: 1.4" > < p > For example, this body text has a line height that’s 1.4 times its font size.</ p > < h2 style = "font-size: 200%" > And this heading also has a line-height that’s 1.4 times the h2’s larger font size, and *not* 1.4 times the body’s font size, which would lead to text overlap</ h2 > </ body >
These number values are generally the preferred values to use for line-height because they inherit better than length values.
3.4. Naming of CSS properties and values
The names of CSS properties are usually nouns, and the names of their values are usually adjectives (although sometimes nouns).
Words in properties and values are separated by hyphens. Abbreviations are generally avoided.
Use the root form of words when possible rather than a form with a grammatical prefix or suffix (for example, "size" rather than "sizing").
The list of values of a property should generally be chosen so that new values can be added. Avoid values like yes , no , true , false , or things with more complex names that are basically equivalent to them.
Avoid words like "mode" or "state" in the names of properties, since properties are generally setting a mode or state.
See § 2.2 Naming things for general (cross-language) advice on naming.
4. JavaScript Language
4.1. Web APIs are for JavaScript
The language that web APIs are meant to be used in, and specified for, is JavaScript (also known as [ECMASCRIPT] ). They are not language-agnostic, and are not meant to be.
This is sometimes a confusing point because [WEBIDL] descended from the language-agnostic OMG IDL (and at one point, included "Java Bindings"). Even today, the structure of the document contains a confusing and redundant division between the "Interface definition language" and the "ECMAScript binding". Rest assured that this division is simply a historical artifact of document structure, and does not imply anything about the intent of Web IDL in general. The only reason it remains is that nobody has taken the time to eradicate it.
As such, when designing your APIs, your primary concern should be with the interface you present to JavaScript developers. You can freely rely upon language-specific semantics and conventions, with no need to keep things generalized.
4.2. Preserve run-to-completion semantics
Web APIs are essentially vehicles for extruding C++- (or Rust-) authored capabilities into the JavaScript code that developers write. As such, it’s important to respect the invariants that are in play in normal JavaScript code. One of the most important of these is run-to-completion semantics : wherein each turn of the JavaScript event loop is processed completely before returning control to the user agent.
In particular, this means that JavaScript functions cannot be preempted mid-execution, and thus that any data observed within the function will stay constant as long as that function is active. This is not the case in other languages, which allow data races via multithreading or other techniques—a C function can be preempted at any time, with the bindings it has access to changing values from one line to the next.
This no-data-races invariant is extensively relied upon in JavaScript programs. As such, the invariant must never be violated—even by web APIs, which are often implemented in languages that do allow data races. Although the user agent may be using threads or other techniques to modify state in parallel , web APIs must never expose such changing state directly to developers. Instead, they should queue a task to modify author-observable state (such as an object property).
4.3. Do not expose garbage collection
Web APIs should not expose a way for author code to deduce when/if garbage collection of JavaScript objects has run.
Although
some
APIs
may
expose
garbage
collection,
such
as
some
implementations
of
getElementsByTagName
,
and
the
JavaScript
WeakRefs
proposal
(which
has
multiple
implementer
support),
API
designers
are
strongly
encouraged
to
avoid
points
in
their
own
APIs
that
depend
on
this
timing.
The reason for this is somewhat subtle. The more that Web API semantics are affected by garbage collection timing (or whether objects are collected at all), the more programs will be affected by changes in this timing. But user agents differ significantly in both the timing of garbage collection and whether certain objects are collected at all, which means the resulting code will be non-interoperable. Worse, according to the usual rules of game theory as applied to browsers, this kind of scenario could force other user agents to copy the garbage collection timing of the original in order to create interoperability. This would cause current garbage collection strategies to ossify, preventing improvement in one of the most dynamic areas of JavaScript virtual machine technology.
In
particular,
this
means
that
you
shouldn’t
expose
any
API
that
acts
as
a
weak
reference,
e.g.
with
a
property
that
becomes
once
garbage
collection
runs.
Such
freeing
of
memory
should
be
entirely
deterministic.
For
example,
currently,
getElementsByTagName
permits
reusing
the
HTMLCollection
which
it
creates,
when
it’s
called
with
the
same
receiver
and
tag
name.
In
practice,
engines
reuse
the
output
if
it
has
not
been
garbage
collected.
This
creates
behavior
which
differs
based
on
the
details
of
garbage
collection,
which
is
strongly
discouraged.
If
getElementsByTagName
were
designed
today,
the
advice
to
the
specification
authors
would
be
to
either
reliably
reuse
the
output,
or
to
produce
a
new
HTMLCollection
each
time
it’s
invoked.
The
case
of
getElementsByTagName
is
particularly
insidious
because
nothing
about
the
API
visibly
relates
to
garbage
collection
details.
By
contrast,
special
APIs
for
this
particular
purpose
such
as
WeakRef
and
FinalizationGroup
have
their
GC
interaction
as
a
specific
part
of
their
contract
with
the
developer.
5. JavaScript API Surface Concerns
5.1. Attributes should behave like data properties
[WEBIDL] attributes are reified in JavaScript as accessor properties, i.e. properties with separate getter and setter functions which can react independently. This is in contrast to the "default" style of JavaScript properties, data properties, which do not have configurable behavior but instead can simply be set and retrieved, or optionally marked read-only so that they cannot be set.
Data property semantics are what are generally expected by JavaScript developers when interfacing with objects. As such, although getters and setters allow infinite customizability when defining your Web IDL attributes, you should endeavor to make the resulting accessor properties behave as much like a data property as possible. Specific guidance in this regard includes:
-
Getters must not have any (observable) side effects.
-
Getters should not perform any expensive operations. (A notable failure of the platform in this regard is getters like
offsetTop
performing layout; do not repeat this mistake.) -
Ensure that your attribute’s getter returns the same object each time it is called, until some occurrence in another part of the system causes a logical "reset" of the property’s value. In particular,
obj
must always hold, and so returning a new value from the getter each time is not allowed.. attribute=== obj. attribute -
Whenever possible, preserve values given to the setter for return from the getter. That is, given
obj
, a subsequent. attribute= xobj
should be true. (This will not always be the case, e.g. if a normalization or type conversion step is necessary, but should be held as a goal for normal code paths.). attribute=== x
If you were thinking about using an attribute, but it doesn’t behave this way, you should probably use a method instead.
5.2. Consider whether objects should be live or static
Objects returned from functions, attribute getters, etc., can either be live or static. A live object is one that continues to reflect changes made after it was returned to the caller. A static object is one that reflects the state at the time it was returned.
Objects that are the way state is mutated are generally live. For example, DOM nodes are returned as live objects, since they are the API through which attributes are set and other changes to the DOM are made. They also reflect changes to the DOM made in other ways (such as through user interaction with forms).
Objects
that
represent
a
collection
that
might
change
over
time
(and
that
are
not
the
way
state
is
mutated)
should
generally
be
returned
as
static
objects.
This
is
because
it
is
confusing
to
users
of
the
API
when
a
collection
changes
while
being
iterated.
Because
of
this,
it
is
generally
considered
a
mistake
that
methods
like
getElementsByTagName()
return
live
objects;
querySelectorAll()
was
made
to
return
static
objects
as
a
result
of
this
experience.
On
the
other
hand,
even
though
URLSearchParams
represents
a
collection,
it
should
be
live
because
the
collection
is
mutated
through
that
object.
Note: It’s possible that some of this advice should be reconsidered for maplike and setlike types, where iterators have reasonable behavior for mutation that happens during iteration. This point likely needs further discussion, and perhaps further experience of use of these types.
It’s
also
worth
considering
the
implications
of
having
live
versus
static
objects
for
the
speed
of
implementations
of
the
API.
When
the
data
needed
by
an
object
are
expensive
to
compute
up-front,
there
is
an
advantage
for
that
object
to
be
live
so
that
the
results
can
be
computed
lazily,
such
as
for
getComputedStyle()
.
On
the
other
hand,
if
the
data
needed
by
an
object
are
expensive
to
keep
up-to-date,
such
as
for
the
NodeList
returned
from
querySelectorAll()
,
then
providing
a
static
object
avoids
having
to
keep
the
object
updated
until
it
is
garbage
collected
(which
may
be
substantially
after
its
last
use).
Likewise, the choice of live versus static objects can influence the memory use of an API. If each call of a method returns a new static object, and the objects are large, then substantial amounts of memory can be wasted until the next garbage collection.
The choice of whether an object is live or static may also influence whether it should be returned from an attribute getter or from a method. See § 5.1 Attributes should behave like data properties . In particular, if a result that changes frequently is returned as a static object, it should probably be returned from a method rather than an attribute getter.
5.3. Prefer dictionary parameters over boolean parameters or other unreadable parameters
APIs should generally prefer dictionary parameters (with named booleans in the dictionary) over boolean parameters. This makes the code that calls the API much more readable . It also makes the API more extensible in the future, particularly if multiple booleans are needed.
For
example,
is
much
more
readable
than
.
Furthermore,
the
booleans
in
dictionaries
need
to
be
designed
so
that
they
all
default
to
false.
If
booleans
default
to
true,
then
users
of
the
API
will
find
unexpected
JavaScript
behavior
since
and
will
produce
different
results.
But
at
the
same
time,
it’s
important
to
avoid
naming
booleans
in
negative
ways,
because
then
code
will
have
confusing
double-negatives.
These
pieces
of
advice
may
sometimes
conflict,
but
the
conflict
can
be
avoided
by
using
opposite
words
without
negatives,
such
as
“repeat”
versus
“once”,
“isolate”
versus
“connect”,
or
“private”
versus
“public”.
Likewise,
APIs
should
use
dictionary
parameters
to
avoid
other
cases
of
difficult
to
understand
sequences
of
parameters.
For
example,
window
is
more
readable
than
window
.
5.4. Make non-critical function parameters optional
Parameters that are not strictly necessary should be made optional. Making arguments optional with reasonable defaults reduces overhead when the function is used, and increases user-friendliness.
Optional parameters should be consolidated into a dictionary, and empty parameters should be treated identically to when the parameter is omitted.
For
example,
element
is
equivalent
to
element
,
and
element
is
equivalent
to
element
.
Exceptions have been made for legacy interoperability reasons (such as XMLHttpRequest), but this should be considered a design mistake rather than recommended practice.
5.5. Classes should have constructors when possible
By
default,
[WEBIDL]
interfaces
are
reified
in
JavaScript
as
"non-constructible"
classes:
trying
to
create
instances
of
them
using
the
normal
pattern
in
JavaScript,
new
X()
,
will
throw
a
TypeError
.
This
is
a
poor
experience
for
JavaScript
developers,
and
doesn’t
match
the
design
of
most
of
the
JavaScript
standard
library
or
of
the
classes
JavaScript
developers
themselves
create.
From a naive perspective, it’s not even coherent: how can instances of this thing ever exist, if they can’t be constructed? The answer is that the platform has magic powers: it can create instances of Web IDL-derived classes without ever actually calling their constructor.
You should strive to reduce such magic in designing your own APIs, and give them constructors which allow JavaScript developers to create them, just like the platform does. This means adding an appropriate constructor operation to your interface, and defining the algorithm for creating new instances of your class.
Apart from reducing the magic in the platform, adding constructors allows JavaScript developers to create instances of the class for their own purposes, such as testing, mocking, or interfacing with third-party libraries which accept instances of that class. Additionally, because of how JavaScript’s class design works, it is only possible to create a subclass of the class if it is constructible.
Sometimes this isn’t the right answer: for example, some objects essentially wrap handles to privileged resources, and it isn’t reasonable to create some representation of that privileged resource in JavaScript so that you can pass it to the constructor. Or the object’s lifecycle needs to be very carefully controlled, so that creating new ones is only possible through privileged operations. Or the interface represents an abstract base class for which it doesn’t make sense to construct an instance, or create an author-defined subclass. But cases like these should be the exception, not the norm.
The
Event
class,
and
all
its
derived
interfaces,
are
constructible.
This
allows
JavaScript
developers
to
synthesize
instances
of
them,
which
can
be
useful
in
testing
or
shimming
scenarios.
For
example,
a
library
designed
for
MouseEvent
instances
can
still
be
used
by
another
library
that
accepts
as
input
TouchEvent
instances,
by
writing
a
small
bit
of
adapter
code
that
creates
synthetic
MouseEvent
s.
The
DOMTokenList
class
is,
very
unfortunately,
not
constructible
.
This
prevents
the
creation
of
custom
elements
that
expose
their
token
list
attributes
as
DOMTokenList
s.
No
HTML
elements
are
constructible.
This
is
largely
due
to
some
unfortunate
design
decisions
that
would
take
some
effort
to
work
around
.
In
the
meantime,
for
the
purposes
of
the
customized
built-in
elements
feature,
they
have
been
made
subclassable
through
a
specifically-designed
one-off
solution:
the
[
HTMLConstructor
]
extended
attribute.
The
Window
class
is
not
constructible,
because
creating
a
new
window
is
a
privileged
operation
with
significant
side
effects.
Instead,
the
window.open()
method
is
used
to
create
new
windows.
The
ImageBitmap
class
is
not
constructible,
as
it
represents
an
immutable,
ready-to-paint
bitmap
image,
and
the
process
of
getting
it
ready
to
paint
must
be
done
asynchronously.
Instead,
the
createImageBitmap()
method
is
used
to
create
it.
Several
non-constructible
classes,
like
Navigator
,
History
,
or
Crypto
,
are
non-constructible
because
they
are
singletons
representing
access
to
per-window
information.
In
these
cases,
the
Web
IDL
namespace
feature
or
some
evolution
of
it
might
have
been
a
better
fit,
but
they
were
designed
before
namespaces
existed,
and
somewhat
exceed
the
current
capabilities
of
namespaces.
If
you’re
designing
a
new
singleton,
strongly
consider
using
a
namespace
instead
of
a
non-constructible
class;
if
namespaces
don’t
yet
have
enough
capabilities,
file
an
issue
on
Web
IDL
to
discuss.
For any JavaScript developers still incredulous that it is possible to create an instance of a non-constructible class, we provide the following code example:
const secret= {}; class NonConstructible{ constructor( theSecret= undefined ) { if ( theSecret!== secret) { throw new TypeError( "Illegal constructor." ); } // Set up the object. } }
If
the
author
of
this
code
never
exposes
the
secret
variable
to
the
outside
world,
they
can
reserve
the
ability
to
create
instances
of
NonConstructible
for
themself:
they
can
always
do
.
But
the
outside
world
can
never
successfully
"guess"
the
secret,
and
thus
will
always
receive
a
TypeError
when
trying
to
construct
an
instance
of
NonConstructible
.
From this perspective, what browsers are doing when they create non-constructible classes isn’t magic: it’s just very tricky. The above section is aimed at urging browsers to avoid such tricks (or their equivalents in C++/Rust/etc.) whenever reasonable.
5.6. Design asynchronous APIs using Promises
Asynchronous APIs should generally be designed using promises rather than callback functions. This is the pattern that we’ve settled on for the Web platform, and having APIs consistently use promises means that the APIs are easier to use together (such as by chaining promises). This pattern also tends to produce cleaner code than the use of APIs with callback functions.
Furthermore, you should carefully consider whether an API might need to be asynchronous before making it a synchronous API. An API might need to be asynchronous if:
-
some implementations may (in at least some cases) wish to prompt the user or ask the user for permission before allowing use of the API,
-
implementations may need to consider information that might be stored on disk in order to compute the result,
-
implementations may need to interact with the network before returning the result, or
-
implementations may wish to do work on another thread or in another process before returning the result.
For more information on how to design APIs using promises, and on when to use promises and when not to use promises, see Writing Promise-Using Specifications .
5.7. Cancel asynchronous APIs/operations using AbortSignal
Async
functions
that
need
cancellation
should
take
an
AbortSignal
as
part
of
an
options
dictionary.
Example:
const controller= new AbortController(); const signal= controller. signal; geolocation. read({ signal});
Reusing
the
same
primitive
everywhere
has
multiplicative
effects
throughout
the
platform.
In
particular,
there’s
a
common
pattern
of
using
a
single
AbortSignal
for
a
bunch
of
ongoing
operations,
and
then
aborting
them
(with
the
corresponding
AbortController
)
when
e.g.
the
user
presses
cancel,
or
a
single-page-app
navigation
occurs,
or
similar.
So
the
minor
extra
complexity
for
an
individual
API
leads
to
a
large
reduction
in
complexity
when
used
with
multiple
APIs
together.
There
might
be
cases
where
cancellation
cannot
be
guaranteed.
In
these
cases,
the
AbortController
can
still
be
used
because
a
call
to
abort()
on
AbortController
is
a
request
to
abort.
How
you
react
to
it
depends
on
your
spec.
Note,
requestAbort()
was
considered
in
the
AbortController
design
instead
of
abort(),
but
the
latter
was
chosen
for
brevity.
5.8. Constants, enums, and bitmasks
In many other platforms and programming languages, constants and enums are commonly expressed using a integer constant, sometimes in conjunction with a bitmask mechanism.
However on the Web platform, it is more common to use a string constant for the cases where a constant is needed. This is much more inspection friendly for both development and expressing the constant codes through a user facing interface, and in JavaScript engines, using integers offers no significant performance benefit over strings.
Strings do not directly address the use case for bitmasks. For these cases, you should use an object dictionary which contains the state that the bitmask is attempting to express, as object dictionaries can then be passed around from method to method as needed as easily as the state in a single bitmask value.
6. Event Design
6.1. Use promises for one time events
Follow the advice in the Writing Promise-Using Specifications guideline.
6.2. Events should fire before Promises resolve
In the case that an asynchronous algorithm (Promise based) intends to dispatch events, then such events should be dispatched before the Promise resolves, rather than after.
This ensures consistency, for instance if you have event handlers changing state, you want all that state to be applied before acting on the resolved promise, as you can subscribe to a promise at any time (before/after it has resolved or rejected).
6.3. Don’t invent your own event listener-like infrastructure
For
recurring
events,
it
could
be
convenient
to
create
a
custom
pair
of
APIs
to
"register"
/
"unregister"
,
"subscribe"
/
"unsubscribe"
,
etc.,
that
take
a
callback
and
get
invoked
multiple
times
until
paired
cancel
API
is
called.
Instead, use the existing event registration pattern and separate API controls to start/stop the underlying process (since event listeners should not have side-effects ).
If
the
callback
would
have
been
provided
specific
data,
then
this
data
should
be
added
to
an
Event
object
(but
see
State
and
Event
subclasses
as
this
is
not
always
necessary).
In
some
cases,
you
can
transfer
the
state
that
would
be
surfaced
in
callback
parameters
into
a
more
persistent
object
which,
in
turn,
can
inherit
from
EventTarget
.
In
some
cases
you
might
not
have
an
object
to
inherit
from
EventTarget
but
it
is
usually
possible
to
create
such
an
object.
For
instance
with
Web
Bluetooth
you
can
add
event
listeners
on
a
Characteristic
object,
which
is
obtained
via
getCharacteristic()
.
If
you
need
to
filter
events,
it
might
be
possible
to
create
a
filter
like
const filter= navigator. nfc. createReadFilter({ recordType: "json" }); const onMessage= message=> { …}; filter. addEventListener( 'exchange' , onMessage);
6.4. Always add event handler attributes
For
an
object
that
inherits
from
EventTarget
,
there
are
two
techniques
available
for
registering
an
event
handler
(e.g.,
an
event
named
"somethingchanged"):
-
addEventListener()
which allows authors to register for the event using the event’s name (i.e.,someobject
) and. addEventListener( "somethingchanged" , myhandler) -
onsomethingchanged
IDL attributes which allow one event handler to be directly assigned to the object (i.e.,someobject
).. onsomethingchanged
Because
there
are
two
techniques
for
registering
events
on
objects
inheriting
from
EventTarget
,
authors
may
be
tempted
to
omit
the
corresponding
event
handler
IDL
attributes
.
They
may
assume
that
event
handler
IDL
attributes
are
a
legacy
registration
technique
or
are
simply
not
needed
given
that
addEventListener()
is
available
as
an
alternative.
However,
it
is
important
to
continue
to
define
event
handler
IDL
attributes
because:
-
they preserve consistency in the platform
-
they enable feature-detection for the supported events (see § 2.3 New features should be detectable )
So,
if
the
object
inherits
from
EventTarget
,
add
a
corresponding
on
yourevent
event
handler
IDL
attribute
to
the
interface.
Note
that
for
HTML
and
SVG
elements,
it
is
traditional
to
add
the
event
handler
IDL
attributes
on
the
GlobalEventHandlers
interface
mixin,
instead
of
directly
on
the
relevant
element
interface(s).
Similarly,
event
handler
IDL
attributes
are
traditionally
added
to
WindowEventHandlers
rather
than
Window
.
6.5. Events are for notification
Try
to
design
DOM
events
to
deliver
after-the-fact
notifications
of
changes.
It
may
be
tempting
to
try
to
trigger
side-effects
from
the
action
of
dispatchEvent()
,
but
in
general
this
is
strongly
discouraged
as
it
requires
changes
to
the
DOM
specification
when
added.
Your
design
will
proceed
more
quickly
if
you
avoid
this
pattern.
6.6. Guard against potential recursion
When designing a long-running or complicated algorithm that is initiated by an API call, events are appropriate for notifying user code of the ongoing process. However, they also introduce the possibility of unexpected re-execution of the current algorithm before it has finished! Because user code gets to run in the middle of the algorithm, recursion can happen if that user code triggers the algorithm again (directly or indirectly).
To address this, if the algorithmic complexity and call graph is reasonably understood, add appropriate state to track that the algorithm is in progress and terminate immediately if the algorithm is already concurrently running. This technique is "guarding" the algorithm:
In
this
example,
an
object
of
type
MyObject
has
a
method
doComplexOpWithEvents()
and
an
internal
state
attribute
[[
started
]]
referenced
below
as
this
.[[
started
]]
whose
initial
value
is
false
.
The
doComplexOpWithEvents()
method
must
act
as
follows:
-
If
this
.[[started
]] is notfalse
then throw anInvalidStateError
DOMException
and terminate this algorithm.Note:
doComplexOpWithEvents()
is not allowed to be run again (e.g., from within a"currentlyinprogress"
event handler) before it finishes its original execution. -
...Do complex stuff...
-
Fire an event named
"currentlyinprogress"
atthis
. -
...Finish up complex stuff...
Note: A caution about early termination: if the algorithm being terminated would go on to ensure some critical state consistency, be sure to also make the relevant adjustments in state before early termination of the algorithm. Not doing so can lead to inconsistent state and end-user-visible bugs when implemented as-specified.
Note: A caution about throwing exceptions in early termination: keep in mind the scenario in which developers will be invoking the algorithm, and whether they would reasonably expect to handle an exception in this [perhaps rare] case. For example, will this be the only exception in the algorithm?
Sometimes "guarding" as illustrated above cannot be done because of the complexity of the algorithm, the number of potential algorithm entry-points, or the side-effects beyond the algorithm’s control. In these cases another form of protecting the integrity of the algorithm’s state is to "defer" the event to a subsequent task or microtask. Deferral ensures that any stack-based recursion is avoided (but does not eliminate potentially problematic loops, as they could now occur as unending follow-up tasks).
Deferring an event is often specified as " queue a task to fire an event ...".
By
way
of
illustration,
the
deprecated
Mutation
Events
fired
in
the
middle
of
node
insertion
and
removal
algorithms,
and
could
be
triggered
by
a
variety
of
common
API
calls
like
appendChild()
.
The
side-effects
from
recursively
running
node
mutation
algorithms
caused
many
years
of
security
issues.
A
more
robust
approach
embodied
in
MutationObserver
applies
the
principle
of
deferral
of
notification
—
in
this
case
to
the
microtask
queue
following
the
algorithm.
Also note that deferral of events is always necessary if the algorithm that triggers the event could be running on a different thread or process. In this case deferral ensures the events can be processed on the correct task in the task queue .
Both approaches to protecting against unwanted recursion have trade-offs. Some things to consider when choosing the guarding approach:
-
all state management can be rationalized within one "turn" of the algorithm (no need to consider any state changes or API invocations between the time the algorithm completes and when the event fires).
-
events can reveal internal state changes as early as possible when dispatched immediately in the algorithm (resulting in minimal delay to user code that wants to act on that state).
-
events do not need to carry snapshots (copies) of internal state because user code running in the event handler can observe relevant state directly on the instance object they were fired on (avoiding the need to derive a new type of
Event
to hold such state snapshots).
When deferring, event handlers will run after the algorithm ends (or starts to run in parallel ) and any number of other tasks or microtasks may run in between that invalidate the object’s state. Since the object’s state will be unknown when the deferred event is dispatched, consider the following:
-
state relevant to the event should be packaged with the deferred event, usually involving a new
Event
-derived type with new attributes to hold the state. For example, theProgressEvent
addsloaded
,total
, etc. attributes to hold the state. -
any coordination needed among parts of an algorithm using deferred events often requires defining an explicit state machine (well-defined state transitions) to ensure that when a deferred event fires, the behavior of inspecting or changing state is well-defined. For example, in [payment-request] , the
PaymentRequest
's [[state]] internal slot explicitly tracks the object’s state through its well-defined transitions.-
in addition to defining state transitions, each coordinated algorithm usually applies the guarding technique (anyway) to ensure the algorithm can only proceed under the appropriate set of states. For example, in [payment-request] note the guards used often around the [[state]] internal slot such as in the
complete()
algorithm.
-
Finally,
dispatching
a
deferred
event
that
does
not
seem
to
require
packaging
extra
state
or
defining
a
state
machine
for
the
algorithm
as
mentioned
above,
could
mean
that
all
of
the
state
transitions
have
been
completed
and
that
the
event
is
meant
to
signal
completion
of
the
algorithm.
In
this
case,
it’s
likely
that
instead
of
using
an
event
to
signal
completion,
the
API
should
be
designed
to
return
and
complete
a
Promise
instead.
See
§ 6.1
Use
promises
for
one
time
events
.
Note: events that expose the possibility of recursion as described in this section were sometimes called "synchronous events". This terminology is discouraged as it implies that it is possible to dispatch an event asynchronously. All events are dispatched synchronously. What is more often implied by "asynchronous event" is to defer firing an event.
6.7.
State
and
Event
subclasses
It’s
tempting
to
create
subclasses
of
Event
for
all
event
types.
This
is
frequently
unnecessary.
Consider
subclassing
Event
when
adding
unique
methods
and
large
amounts
of
state.
In
all
other
cases,
using
a
"vanilla"
event
with
state
captured
in
the
target
object.
6.8. How to decide between Events and Observers
Several
recent
additions
to
the
platform
employ
an
Observer
pattern.
MutationObserver
,
IntersectionObserver
,
ResizeObserver
,
and
IndexedDB
Observers
provide
precedents
for
new
Observer
types.
Many
designs
can
be
described
as
either
Observers
or
EventTarget
s.
How
to
decide?
In
general,
start
your
design
process
using
an
EventTarget
and
only
move
to
Observers
if
and
when
events
can’t
be
made
to
work
well.
Using
an
EventTarget
ensures
your
feature
benefits
from
improvements
to
the
shared
base
class,
such
as
the
recent
addition
of
the
once
.
Observers have the following properties:
-
Each instance is constructed with a callback, and optionally with some global options.
-
Instances observe specific targets, using the
observe()
andunobserve()
methods. The callback provided in the constructor is invoked when something interesting happens to those targets. -
Callbacks receive change records as arguments. These records contain the details about the interesting thing that happened. Multiple records can be delivered at once.
-
Observation can be customized with additional options passed to
observe()
. -
disconnect()
stops observation. -
takeRecords()
synchronously returns records for all observed-but-not-yet-delivered occurrences.
MutationObserver
takes
a
callback
which
receives
MutationRecord
s.
It
cannot
be
customized
at
construction
time,
but
each
observation
can
be
customized
using
the
MutationObserverInit
set
of
options.
It
observes
Node
s
as
targets.
IntersectionObserver
takes
a
callback
which
receives
IntersectionObserverEntry
s.
It
can
be
customized
at
construction
time
using
the
IntersectionObserverInit
set
of
options,
but
each
observation
is
not
further
customizable.
It
observers
Element
s
as
targets.
Observers involve defining a new class, dictionaries for options, and a new type for the delivered records. For the cost, you gain a few advantages:
-
Instances can be customized at observation time. The
observe()
method of anObserver
can take options, allowing per-callback customization. This is not possible withaddEventListener()
. -
Reuse of their creation-time customizations on multiple targets.
-
Easy disconnection from multiple targets via
disconnect()
. -
Built-in support for synchronously probing system state. Both
Observer
s andEventTarget
s can batch occurrences and deliver them later, butObserver
s have atakeRecords()
method which allows synchronously probing waiting until the next batched delivery. -
Because they are single-purpose, you don’t need to specify an event type.
Observer
s
and
EventTarget
s
overlap
in
the
following
ways:
-
Both can be customized at creation time.
-
Both can batch occurrences and deliver them at any time.
EventTarget
s don’t need to be synchronous; they can use microtask timing, idle timing, animation-frame timing, etc. You don’t need anObserver
to get special timing or batching. -
Neither
EventTarget
s norObserver
s need participate in a DOM tree (bubbling/capture and cancellation). Most prominentEventTarget
s areNode
s in the DOM tree, but many other events are standalone; e.g.IDBDatabase
andXMLHttpRequestEventTarget
. Even when usingNode
s, you can leave your events can be designed to be non-bubbling and non-cancelable to get thatObserver
-esque feel.
IntersectionObserver
that
is
an
EventTarget
subclass:
const io= new ETIntersectionObserver( element, { root, rootMargin, threshold}); function listener( e) { for ( const changeof e. changes) { // ... } } io. addEventListener( "intersect" , listener); io. removeEventListener( "intersect" , listener);
As
you
can
see,
we’ve
lost
some
functionality
compared
to
the
Observer
version:
the
ability
to
easily
observe
multiple
elements
with
the
same
options,
or
the
takeRecords()
and
disconnect()
methods.
We’re
also
forced
to
add
the
rather-redundant
"intersect"
event
type
to
our
subscription
calls.
However,
we
haven’t
lost
the
batching,
timing,
or
creation-time
customization,
and
the
ETIntersectionObserver
doesn’t
participate
in
a
hierarchy.
These
aspects
can
be
achieved
with
either
design.
7. Types and Units
7.1. Use numeric types appropriately
[WEBIDL] contains many numeric types. However, it is very rare that its more specific ones are actually appropriate.
JavaScript
has
only
one
numeric
type,
Number:
IEEE
754
double-precision
floating
point,
including
±0,
±Infinity,
and
NaN
(although
thankfully
only
one).
The
Web
IDL
"types"
are
coercion
rules
that
apply
when
accepting
an
argument
or
triggering
a
setter.
For
example,
a
Web
IDL
unsigned
short
roughly
says:
"when
someone
passes
this
as
an
argument,
take
it
modulo
65536
before
doing
any
further
processing".
That
is
very
rarely
a
useful
thing
to
do.
Instead, you will want to stick with one of:
-
unrestricted double
-
When truly any JavaScript number will do, including infinities and NaN
-
double
-
Any JavaScript number excluding infinities and NaN
-
[EnforceRange] long long
-
Any JavaScript number in the integer-representable range, throwing a
TypeError
outside the range and rounding inside of it -
[EnforceRange] unsigned long long
-
Any nonnegative JavaScript number in the integer-representable range, throwing a
TypeError
outside the range and rounding inside of it -
bigint
-
When the possible integer values range more broadly than JavaScript numbers can accurately represent
Additionally, you can combine any of the above with an extra line in your algorithm to validate that the number is within the expected domain-specific range, and throwing or performing other actions in response. (While it is very rarely appropriate to modify author input by taking it modulo 65536, it might be appropriate to take it modulo 360, for example.)
A
special
case
of
domain-specific
validation,
which
Web
IDL
already
has
you
covered
for,
is
the
0–255
range.
This
can
be
written
as
[EnforceRange]
octet
:
any
JavaScript
number
in
the
range
0–255,
throwing
a
TypeError
outside
the
range
and
rounding
inside
of
it.
(And
indeed,
if
it
turns
out
that
the
other
power-of-two
ranges
are
semantically
meaningful
for
your
domain,
such
that
you
want
the
modulo
or
range-checking
behavior,
feel
free
to
use
them.)
long
long
and
unsigned
long
long
only
have
53
bits
of
precision,
and
not
64.
bigint
is
recommended
to
be
used
only
in
circumstances
where
a
[EnforceRange]
long
long
or
[EnforceRange]
unsigned
long
long
would
not
work
in
terms
of
expressivity:
that
is,
only
when
values
greater
than
2
53
or
less
than
-2
53
are
expected.
In
particular,
APIs
should
not
use
the
bigint
type
simply
because
the
input
is
an
integer—
bigint
is
for
big
integers
(hence
the
name!).
Transparent support for inputs being either JavaScript Number of BigInt types is discouraged, unless there is a domain-specific requirement to support it. As far as ergonomics, JavaScript developers are expected to keep track of whether their values are Numbers or BigInts. Sloppiness or implicit conversions here could lead to a loss of precision, defeating the purpose of using BigInt.
Further,
try
to
resist
doubling
up
the
surface
area
of
an
API
in
order
to
provide
both
BigInt
and
Number
variants
of
an
API.
As
an
API
designer,
consider
it
your
job
to
chose
the
correct
numeric
type
for
the
feature
being
exposed.
7.2. Represent strings appropriately
When
designing
a
web
platform
feature
which
operates
on
strings
,
DOMString
is
almost
always
the
right
Web
IDL
string
type
to
use.
This
is
because
most
string
operations
don’t
need
to
interpret
the
code
units
inside
of
the
string.
There
are
exceptions,
however,
for
which
Web
IDL
provides
USVString
and
ByteString
.
[INFRA]
[WEBIDL]
USVString
is
the
Web
IDL
type
that
represents
scalar
value
strings
.
For
strings
whose
most
common
algorithms
operate
on
scalar
values
(such
as
percent-encoding
),
or
for
operations
which
cannot
handle
surrogates
in
input
(such
as
APIs
that
pass
strings
through
to
native
platform
APIs),
USVString
should
be
used.
Reflecting
IDL
attributes
whose
content
attribute
is
defined
to
contain
a
URL
(such
as
href
)
should
use
USVString
.
[HTML]
ByteString
should
only
be
used
for
representing
data
from
protocols
like
HTTP
which
don’t
distinguish
between
bytes
and
strings.
It
is
not
a
general-purpose
string
type.
If
you
need
to
represent
a
sequence
of
bytes
,
use
Uint8Array
.
7.3. Use milliseconds for time measurement
Any
web
API
that
accepts
a
time
measurement
should
do
so
in
milliseconds.
This
is
a
tradition
stemming
from
setTimeout
and
the
Date
API,
and
carried
through
since
then.
Even if seconds (or some other unit) are more natural in the domain of an API, sticking with milliseconds ensures interoperability with the rest of the platform, allowing easy arithmetic with other time quantities.
Note that high-resolution time is usually represented as fractional milliseconds, not e.g. as nanoseconds.
7.4. Use the appropriate type to represent times and dates
When
representing
date-times
on
the
platform,
use
the
DOMTimeStamp
type,
with
values
being
the
number
of
milliseconds
relative
to
1970-01-01T00:00:00Z.
The
JavaScript
Date
class
must
not
be
used
for
this
purpose.
Date
objects
are
mutable
(and
there
is
no
way
to
make
them
immutable),
which
comes
with
a
host
of
attendant
problems.
Date
must
not
be
used,
see
the
following:
-
Frozen date objects? on es-discuss
-
Remove Date from Web IDL on the Web IDL Bugzilla
While
in
theory
time
is
monotonically
increasing,
values
like
DOMTimeStamp
that
represent
time
since
1970
are
derived
from
the
system’s
clock,
which
may
sometimes
move
backwards
(for
example,
from
NTP
or
manual
adjustment),
causing
the
timestamp
values
to
decrease
over
time.
They
may
also
remain
the
same
due
to
the
limitation
of
millisecond
resolution.
Thus,
for
time
stamps
that
do
not
need
to
correspond
to
an
absolute
time,
consider
using
DOMHighResTimeStamp
,
which
provides
monotonically
non-decreasing
sub-millisecond
timestamps
that
are
comparable
within
a
single
browsing
context
or
web
worker.
See
[HIGHRES-TIME]
for
more
details.
7.5. Use Error or DOMException for errors
Errors
in
web
APIs
should
be
represented
as
ECMAScript
error
objects
(e.g.,
Error
)
or
as
DOMException
.
There
was
at
one
point
a
trend
to
use
DOMError
when
objects
had
a
property
representing
an
error.
However,
we
no
longer
believe
there
was
value
in
this
split,
and
therefore
suggest
that
ECMAScript
error
objects
(e.g.,
TypeError
)
or
DOMException
should
be
used
for
errors,
whether
they
are
exceptions,
promise
rejection
values,
or
properties.
8. OS and Device Wrapper APIs
It is increasingly common to see new APIs developed in the web platform for interacting with devices. For example, authors wish to be able to use the web to connect with their microphones and cameras , generic sensors (such as gyroscope and accelerometer), Bluetooth and USB -connected peripherals, automobiles , toothbrush, etc.
These can be functionality provided by the underlying operating system, or provided by a native third-party library to interact with a device. These are an abstraction which "wrap" the native functionality without introducing significant complexity while securing the API surface to the browser, hence are called wrapper APIs.
This section contains principles for consideration when designing APIs for devices.
8.1. Use care when exposing identifying information about devices
Merely exposing the presence of a device may harm user privacy. Exposing additional information about a device or its capabilities increases this risk further. Exposing device identifiers increases this risk even more. Any such information increases the set of fingerprinting data available to sites. But there are possible user privacy harms here that go beyond fingerprinting. Please read [FINGERPRINTING-GUIDANCE] and [UNSANCTIONED-TRACKING] for additional details.
Think carefully about whether it is really necessary to expose identifying information about the device at all. Please consider if your use cases could be satisfied by a less powerful API. [LEAST-POWER]
Despite these concerns, you may find that it is necessary to expose identifying information about a device to the web platform.
The following guidelines will help ensure that this is done in a consistent and privacy-friendly way:
- Limit identifiable information in the id
-
As much as possible, device ids exposed to the web platform should not contain identifiable information such as branding, make and model numbers, etc. In many cases using a randomized number or unique id is preferable to a generic string identifier such as "device1".
Device ids expressed as numbers should contain sufficient entropy so as to avoid re-use or potential sharing among other devices, and should not be easily guessable.
- Keep the user in control
-
Any existing device ids mapped to or stored with the current session by the user agent should be cleared when users elect to "clear their cookies" (and other related settings). Above all, the user should be in control of this potential tracking state and be able to reset it on demand.
- Hide sensitive ids behind a user permission
-
Where device identification does not make sense to be expressed in an anonymous way, access to the identifier should be limited by default. One way to limit exposure is to only surface the identifier to author code after obtaining permission from the user.
- Tie ids to the same-origin model
-
Identifiers should be unique to the origin of the web content that is attempting to access them. Web content from one origin should receive an identifier that is distinct from the identifier given to web content from any other origin despite the physical device being the same.
Within an origin, ids may have a stable representation to web content. This ensures a consistent developer experience if requesting the same device twice.
- Persistable when necessary
-
Some device identifiers can only be obtained via a complex or time-consuming device selection process. In such cases, it may make sense for author code to be able to persist such identifiers for use in a later session in order to avoid the selection process a second time. When this is the case, the API should not only provide a stable id during the session for the given origin, but it should also be capable of deterministically producing the same id in subsequent sessions.
8.2. Use care when exposing APIs for selecting or enumerating devices
APIs which expose the the existence, capabilities, and/or identifiers of many devices risk multiplying the harm described in § 8.1 Use care when exposing identifying information about devices (since they expose fingerprinting entropy per exposed device) and thus require additional consideration.
As in the previous section, please consider if your use cases could be satisfied by a less powerful API. [LEAST-POWER]
If the purpose of the API is to enable the user to select a device from the set of available devices of a particular kind, you may not need to expose a list to script at all. An API which invokes a User-Agent-provided device picker could suffice. Such an API keeps the user in control, hides the device behind a user permission, does not expose any fingerprinting data about the user’s environment by default, and, when used, only exposes information about one device.
<input
type=file>
)
invoke
a
User
Agent-provided
file
picker.
This
elegant
design
ensures
that
the
website
only
gets
access
to
file(s)
the
user
explicitly
selected,
and
does
not
receive
any
other
information
about
the
user’s
file
system.
[HTML]
When designing such an API, it may be necessary to also expose the fact that there are devices are available to be picked. Such a feature exposes a small amount (one bit) of fingerprinting data about the user’s environment to websites, so it isn’t quite as safe as an API which does not have such a feature.
The
RemotePlayback
interface
does
not
expose
a
list
of
available
remote
playback
devices
.
Like
<input
type=file>
,
it
invokes
a
User
Agent-defined
device
picker,
which
has
the
same
desirable
properties.
[REMOTE-PLAYBACK]
Unlike
<input
type=file>
,
it
also
enables
websites
to
detect
whether
or
not
any
remote
playback
device
is
available,
so
the
website
can
show
or
hide
a
control
the
user
can
use
to
invoke
the
picker.
If you must expose a list of devices, try to expose the smallest subset that satisfies your use cases. Consider an API which allows the website to request a filtered or constrained list of devices, but keep in mind that constraint-matching systems may reveal more information than intended when repeatedly invoked with different constraints.
If you must expose the full list of devices of a particular kind, please rigorously define the order in which devices will be listed. This can reduce interoperability issues, and helps to mitigate fingerprinting (because sort order could reveal other information; see Fingerprinting Guidance §5.2 Standardization for more.)
While APIs should not expose a full list of devices in an implementation-defined order, they may need to for web compatibility reasons.
8.3. Native APIs don’t typically translate well to the web
Many modern operating systems come with convenience APIs, which abstract away normally complex technology in the form of higher level user-friendly APIs.
Exposing these APIs to the web platform is getting more and more common, but you need to be careful when exposing these to the web platform.
- Generalize interface when underlying API is overly specific
-
Don’t blindly map native convenience APIs 1:1 to the web platform. Native APIs do not translate well to the web platform, and doing so may result in a API shape which is difficult to implement across a variety of underlying platforms and native libraries.
Especially be careful about exposing the exact lifecycle and data structures of the underlying native APIs. Think about making an API that fits well in with existing web platform APIs and which can be expressed in commonly available low-level APIs.
- Prefer asynchronous function by returning promises
-
Even if the underlying API is synchronous, this does not necessarily mean it translates well when ported to the web platform. Exposing a synchronous native API to the web platform is generally discouraged, and should be provided as a promise based API whenever possible.
Doing this also allows putting the API behind a permission if it is necessary, especially if the API depends on I/O or in the future may need to be isolated to a separate process.
- Don’t overconstrain or overfit the API to the underlying (wrapped) OS
-
When designing a wrapper API, you should consider how different implementations (operating systems and platforms) provide this functionality. Ideally the entire specification is implementable across platforms, but in some cases it may be desirable to expose distinctions made on only some platforms.
These distinctions should be clearly noted if they are known in advance.
- Do not propose black-box proprietary library or dedicated hardware processor dependencies
-
Black-box dependencies are strongly discouraged, as this not only prevents wide implementor adoption, but is unhealthy to the open nature of the web.
- Underlying protocols should be open
-
APIs which require exchange with external hardware or services should not depend on closed protocols.
- Be offline friendly
-
Web platform APIs should ideally not have a hard dependency on online services. The availability of these services across different regions may vary, users may be offline, and the online services have no guarantee of being always available.
- Avoid additional fingerprinting surfaces
-
Wrapper APIs can expose unintentionally expose the user to a wider fingerprinting surface. Please read the TAG’s finding on unsanctioned tracking for additional details.
9. Other API Design Considerations
9.1. Privacy, Security, Accessibility, and Internationalization
It is important not to neglect other aspects of API design such as privacy, security, accessibility, and internationalization. Please take advantage of these other excellent resources in your design process:
9.2. Polyfills
Polyfills can be hugely beneficial in helping to roll out new features to the web platform. The Technical Architecture Group finding on Polyfills and the Evolution of the Web offers guidance that should be considered in the development of new features, notably:
-
Being "polyfillable" is not essential but is beneficial
-
Polyfill development should be encouraged
9.3. Extend existing manifest files rather than creating new ones
New web features should be self-contained and self-describing and ideally should not require an additional manifest file. Manifest files can add complexity to the development and deployment process for web technologies. Sometimes manifest files are required, but the use of different manifests on the web should be kept to a minimum.
You may need to make a new manifest file if the domain of the manifest file is different from the existing manifest files. For example, if the fetch timing is different, or if the complexity of the manifest warrants it. Application metadata should be added to the Web App Manifest or be an extension of it. Manifests designated to be used for specific applications or which require interoperability with non-browsers may need to take a different approach. Payment Method Manifest, Publication Manifest, and Origin Policy are examples of these cases.
For example, if you have a single piece of metadata, even if the fetch timing is different than an existing manifest, it is probably best to use an existing manifest (or ideally design the feature in such a way that a manifest is not required). However, if your feature requires a complex set of metadata specific to a functional domain, the creation of a new manifest may be justified.
Note that in all cases, the naming conventions should be harmonized (see section 2.2)
Note: By principle, existing manifests use lowercase, underscore-delimited names. There have been times where it has been beneficial to re-use dictionaries from a manifest in DOM APIs as well, which are camel-cased. One such example is the image resource . For this reason, if a key can clearly be expressed as a single word, that is recommended.
When designing new keys and values for a manifest, make sure they are needed (that is, they enable well-thought-out use-cases). Also, please check if a similar key exists. If an existing key/value pair does more or less what is needed, work with the existing spec to extend it to your use-case if possible.
Some of the existing manifest files include
-
Web App Manifest which contains features related to web applications.
-
Payment Method Manifest which is used for payment methods in the context of the web payment API
-
Publication Manifest which is used by some web publications working group standards
-
Origin Policy which is used to set security policies.
We encourage people to think of manifest files as extensible. Always try to get the changes into the original spec, or at least discuss the extension with the spec editors. Having this discussion is more likely to result in a better design and lead to something that better integrates with the platform.
There are certain times the spec authors might not want to integrate immediately due to process (like going to CR) or due to it having a different scope, like extensions to Web App Manifest only affecting store or payment use-cases. In that case, it is acceptable to monkey-patch as long as that is agreed with the original spec editors.
when we write up a principle on monkey patching, be sure to take this nuance into account. <https://github.com/w3ctag/design-principles/issues/184>
An example of something that was done as a monkey patch that is scheduled to be integrated into the web app manifest in a future level (post-CR):
-
https://wicg.github.io/web-share-target/#extension-to-the-web-app-manifest
10. Writing good specifications
This document mostly covers API design for the Web, but those who design APIs are hopefully also writing specifications for the APIs that they design.
10.1. Identify the audience of each requirement in your specification
Document both how authors should write good code using your API, and how implementers of your API should handle poorly-written code.
The
web
platform
is,
unusually,
web,
especially
in
comparison
to
other
platforms,
is
designed
to
be
robust
in
accepting
poorly-formed
markup.
When
writing
specifications
for
the
web,
This
means
that
web
pages
which
use
older
versions
of
web
standards
can
still
be
sure
to
note
authoring
requirements
for
well-formed
markup
(the
"conforming
language"),
viewed
in
newer
user
agents,
and
also
implementation
requirements
for
handling
that
authors
have
a
shallower
learning
curve.
To
support
this,
web
specification
writers
need
to
describe
how
to
interpret
poorly-formed
markup
(the
"supported
language").
markup,
as
well
as
well-formed
markup.
This
allows
us
Implementers
need
to
have
a
relatively
clean
and
understandable
language
be
able
to
understand
the
"supported
language",
which
is
more
complex
than
the
"conforming
language"
which
authors
should
be
aiming
to
use.
<table>
element
explains
how
to
process
the
contents
of
<table>
element,
including
cases
where
the
contents
do
not
conform
to
the
Content
model
.10.2. Specify completely and avoid ambiguity
If
the
specification
leaves
room
for
implementations
to
When
specifying
how
a
feature
should
work,
make
different
choices
which
mean
sure
that
there
is
enough
information
so
that
authors
need
don’t
have
to
write
different
markup
for
code
to
work
with
different
implementations,
this
adds
a
maintenance
burden
for
authors.
implementations.
The
If
a
specification
should
be
complete
enough
for
implementations
isn’t
specific
enough,
implementers
might
make
different
choices
which
force
authors
to
be
interoperable,
without
depending
on
or
referring
write
extra
code
to
handle
the
differences.
Implementers
shouldn’t
need
to
check
details
of
other
implementations.
When
there
isn’t
a
good
reason
implementations
to
allow
variation
between
implementations,
it
is
important
that
things
avoid
this
situation.
Instead,
the
specification
should
be
specified
unambiguously,
so
that
implementations
interoperate
complete
and
web
content
doesn’t
end
up
depending
clear
enough
on
the
implementation-defined
behavior
in
one
implementation
and
failing
in
another
(which
can
cause
higher
development
costs
to
fix
the
content
for
all
browsers,
or
costs
to
users
of
broken
content
if
it
wasn’t
detected).
its
own.
Note:
This
way,
it
is
easier
to
author
content
doesn’t
mean
that
works
in
a
variety
of
user
agents.
However,
implementations
can’t
render
things
differently,
or
show
different
user
interfaces
for
things
like
permission
prompts.
Note:
Implementers
should
still
be
free
file
bugs
against
specifications
which
don’t
give
them
clear
enough
information
to
make
improvements
in
areas
such
as
user
interface
and
quality
of
rendering.
write
the
implementation.
10.2.1. Defining algorithms in specifications
Specification
text
Write
algorithms
in
a
way
that
is
often
more
precise
when
it
clear
and
concise.
The
most
common
way
to
write
algorithms
is
written
using
to
write
an
explicit
sequence
of
steps.
Using
an
ordered
sequence
of
steps
makes
it
clear
how
the
required
behaviors
interact.
For
example,
if
there
are
multiple
error
conditions
present,
a
specification
that
describes
the
behavior
using
This
often
looks
like
pseudo-code.
showModal()
method
is
described
as
a
numbered
sequence
of
steps
When
there
are
particular
characteristics
writing
a
sequence
of
steps,
imagine
that
specification
editors
want
to
ensure
it
is
a
piece
of
functional
code.
Clearly specify the
specification always satisfies,inputs and outputs, name the algorithm and the variables it uses well, and explicitly note the points in the algorithm where the algorithm maybe better to usereturn aless algorithmic way of formal specification that ensures these characteristics are always true. Developing these other waysresult or error.As much as possible, avoid writing algorithms which have side effects.
Summarize
the
purpose
of
specifying
behavior
formally
is
more
important
when
the
algorithms
would
be
more
repetitive.
These
formal
specification
techniques
algorithm
before
going
into
detail,
so
that
readers
can
include
dependencies
on
languages
such
as
[WEBIDL]
,
on
grammar
productions,
decide
whether
to
read
the
steps
or
skip
over
them.
For
example
take
the
development
of
new
languages
for
defining
pieces
following
steps,
which
ensure
that
there
is
at
most
one
pending
X
callback
per
top-level
browsing
context.
A
plain
sequence
of
the
specification.
Merely
having
a
formal
syntax
steps
is
not
sufficient,
though;
the
requirements
that
always
the
formal
definitions
place
on
implementations
must
still
be
clearly
defined,
which
generally
requires
specifying
some
algorithms
(or,
sometimes,
another
layer
of
formal
syntax)
that
best
way
to
write
an
algorithm.
For
example,
it
might
make
sense
to
define
what
the
formal
syntax
means,
and
then
using
the
more-readable
or
re-use
a
formal
syntax
or
grammar
to
avoid
excessive
repetition
of
the
less-readable
algorithms.
There
are
also
possibilities
in-between,
for
example,
if
it
is
desirable
repetition,
or
define
specific
states
to
ensure
that
an
algorithm
can
be
implemented
with
used
in
a
state
machine,
it
may
be
preferable
to
specify
it
in
an
algorithmic
way
that
incorporates
explicit
states
so
that
machine.
When
using
extra
constructs
like
these,
the
specification
authors
avoid
accidentally
creating
more
states
than
intended.
earlier
advice
still
applies.
When
specifying
in
an
algorithmic
way,
it
is
generally
good
to
match
the
As
much
as
possible,
describe
algorithms
that
implementations
would
use,
even
if
this
is
somewhat
less
convenient
for
specification
authors,
as
long
closely
as
doing
so
possible
to
how
they
would
not
significantly
increase
be
implmented.
This
may
make
the
specification’s
complexity.
For
example,
it
is
better
to
specify
CSS
selector
matching
by
moving
right
spec
harder
to
left
across
combinators
,
since
write,
but
it
means
that
is
what
implementations
do.
This
avoids
don’t
need
to
figure
out
how
to
translate
what’s
written
in
the
risk
of
future
problems
caused
by
new
features
that
would
specification
to
how
it
should
actually
be
slightly
implemented.
In
particular,
that
may
mean
that
different
(in
unimportant
ways)
between
implementations
based
on
one
algorithm
and
implementations
based
on
a
make
different
one.
Thus,
it
avoids
small
future
divergence
decisions
that
may
lead
to
later
features
being
feasible
in
behavior
between
specification
one
implementation
but
not
another.
CSS
selectors
are
read
and
implementations.
It
also
helps
understood
from
left
to
keep
the
costs
of
specifying
features
aligned
with
implementing
them,
since
the
complexity
of
adding
a
feature
right,
but
in
practice
are
matched
from
right
to
left
in
implementations.
This
allows
the
specification
is
more
likely
most
specific
term
to
be
related
to
the
cost
matched
or
not
matched
quickly,
avoiding
unnecessary
work.
The
CSS
selector
matching
algorithm
is
written
this
way,
instead
of
adding
it
to
implementations.
a
hypothetical
algorithm
which
would
more
closely
match
how
CSS
selectors
are
often
read
by
CSS
authors.
Algorithms
in
specifications
should
also
reflect
best
practices
in
programming.
For
example,
they
should
explicitly
describe
the
inputs
and
outputs
of
the
algorithm,
rather
than
relying
on
"stack
introspection"
or
handwaving.
They
should
also
avoid
side-effects
when
possible.
See
also:
-
[INFRA] offerssome useful definitions and terminologyfor defining algorithms. Specifications that define things using step-by-step algorithms should still be readable. Partly, this is through good practices that also apply to writing code, such as avoiding unneeded extra steps, and by using good naming. However, it’s often useful to explain the purpose of the algorithm in prose (e.g., "take the following steps, which ensure that there is at most one pending X callback per toplevel browsing context") so that readers can quickly decide whether they need to read the steps in detail.from [INFRA]
10.2.2. Use explicit flags for state
Describing
Instead
of
describing
state
using
with
words,
use
explicit
flags
makes
behavior
clearer.
for
state
when
writing
algorithms.
Using explicit flags makes it clear whether or not the state changes in different error conditions, and makes it clear when the state described by the flags is reset.
10.3. Other resources
Some useful advice on how to write specifications is available elsewhere:
-
Writing specifications: Kinds of statements (Ian Hickson, 2006)
-
QA Framework: Specification Guidelines (W3C QA Working Group, 2005)
Acknowledgments
This document consists of principles which have been collected by TAG members past and present during TAG design reviews . We are indebted to everyone who has requested a design review from us.
The TAG would like to thank Adrian Hope-Bailie, Alan Stearns, Aleksandar Totic, Alex Russell, Andreas Stöckel, Andrew Betts, Anne van Kesteren, Benjamin C. Wiley Sittler, Boris Zbarsky, Brian Kardell, Charles McCathieNevile, Chris Wilson, Dan Connolly, Daniel Ehrenberg, Daniel Murphy, Domenic Denicola, Eiji Kitamura, Eric Shepherd, Ethan Resnick, fantasai, François Daoust, Henri Sivonen, HE Shi-Jun, Ian Hickson, Irene Knapp, Jake Archibald, Jeffrey Yasskin, Jeremy Roman, Jirka Kosek, Kevin Marks, Lachlan Hunt, Léonie Watson, L. Le Meur, Lukasz Olejnik, Maciej Stachowiak, Marcos Cáceres, Mark Nottingham, Martin Thomson, Matt Giuca, Matt Wolenetz, Michael[tm] Smith, Mike West, Nick Doty, Nigel Megitt, Nik Thierry, Ojan Vafai, Olli Pettay, Pete Snyder, Philip Jägenstedt, Philip Taylor, Reilly Grant, Richard Ishida, Rick Byers, Ryan Sleevi, Sergey Konstantinov, Stefan Zager, Stephen Stewart, Steven Faulkner, Surma, Tab Atkins-Bittner, Tantek Çelik, Tobie Langel, Travis Leithead, and Yoav Weiss for their contributions to this & the HTML Design Principles document which preceded it.
Special thanks to Anne van Kesteren and Maciej Stachowiak, who edited the HTML Design Principles document.
If you contributed to this document but your name is not listed above, please let the editors know so they can correct this omission.