AMP can be extended to allow more functionality and components through
building open source extensions (aka custom elements). For example, AMP
provides amp-carousel
, amp-sidebar
and amp-access
as
extensions. If you'd like to add an extension to support your company
video player, rich embed or just a general UI component like a star
rating viewer, you'd do this by building an extension.
- Getting started
- Naming
- Directory structure
- Extend AMP.BaseElement
- Element styling
- Register element with AMP
- Actions and events
- Sub-elements ownership
- Allowing proper validation
- Performance considerations
- Layouts supported in your element
- Experiments
- Documenting your element
- Example of using your extension
- Updating build configs
- Versioning
- Unit tests
- Type checking
- Example PRs
This document describes how to create a new AMP extension, which is one of the most common ways of adding a new feature to AMP.
Before diving into the details on creating a new AMP extension, please familiarize yourself with the general process for contributing code and features to AMP. Since you are adding a new extension you will likely need to follow the process for making a significant change, including filing an "Intent to Implement" issue and finding a guide before you start significant development.
To bootstrap the creation of a new component, the following command will create the directory structure and boilerplate code for you:
$ amp make-extension --name=amp-my-element
All AMP extensions (and built-in elements) have their tag names prefixed
with amp-
. Make sure to choose an accurate and clear name for your
extension.
Extensions that embed a third-party service must follow the guidelines for naming a third-party component.
You create your extension's files inside the extensions/
directory.
The directory structure is below:
/extensions/amp-my-element/
├── 0.1/
| ├── test/
| | ├── test-amp-my-element.js # Element's unit test suite (req'd)
| | └── More test JS files (optional)
| ├── amp-my-element.js # Element's implementation (req'd)
| ├── amp-my-element.css # Custom CSS (optional)
| └── More JS files (optional)
├── validator-amp-my-element.protoascii # Validator rules (req'd)
├── amp-my-element.md # Element's main documentation (req'd)
└── More documentation in .md files (optional)
└── OWNERS # Owners file. Primary contact(s) for the extension. (req'd)
In most cases you'll only create the required (req'd) files. If your element does not need custom CSS, you don't need to create the CSS file.
Almost all AMP extensions extend AMP.BaseElement, which provides some hookups and callbacks for you to override in order to implement and customize your element behavior. These callbacks are explained below in the BaseElement Callbacks section, and are also explained inline in the BaseElement class.
The following shows the overall structure of your element implementation file (extensions/amp-my-element/0.1/amp-my-element.js).
import {func1, func2} from '../src/module';
import {CSS} from '../../../build/amp-my-element-0.1.css';
// more ES2015-style import statements.
/** @const */
const EXPERIMENT = 'amp-my-element';
/** @const */
const TAG = 'amp-my-element';
class AmpMyElement extends AMP.BaseElement {
/** @param {!AmpElement} element */
constructor(element) {
super(element);
// Declare instance variables with type annotations.
}
/** @override */
isLayoutSupported(layout) {
return layout == LAYOUT.FIXED;
}
/** @override */
buildCallback() {
// Get attributes, assertions of values, assign instance variables.
// Build lightweight DOM and append to this.element.
}
/** @override */
layoutCallback() {
// Actually load your resource or render more expensive resources.
}
}
AMP.extension('amp-my-element', '0.1', (AMP) => {
AMP.registerElement('amp-my-element', AmpMyElement, CSS);
});
- Default: Does nothing
- Override: Rarely.
- Vsync Context: None
- Usage: If your extension provides different implementations depending on a late runtime condition (e.g. type attribute on the element, platform)
- Example Usage: amp-ad, amp-app-banner
- Default: Does nothing
- Override: Almost always
- Vsync Context: Mutate
- Usage: If your element has UI elements this is where you should create your DOM structure and append it to the element. You can also read the attributes (e.g. width, height…) the user provided on your element in this callback.
- Warning: Don't load remote resources during the buildCallback. This not only circumvents the AMP resources manager, but it will also lead to higher data charges for users because all these resources will be loaded before layouting needs to happen.
- Warning 2: Do the least needed work here, and don't build DOM that is not needed at this point.
- Default: Does nothing.
- Vsync Context: None (Neither mutate nor measure)
- Override: Sometimes, if your element will be loading remote resources.
- Usage: Use to instruct AMP which hosts to preconnect to, and which resources to preload/prefetch; this allows AMP to delegate to the browser to get a performance boost by preconnecting, preloading and prefetching resources via preconnect service.
- Example Usage: Instagram uses this to preconnect to instagram hosts.
- Default: Does nothing.
- Vsync Context: Mutate
- Override: Sometimes. If your component provides a way to dynamically create a lightweight placeholder. This gets called only if the element doesn't already have a publisher-provided placeholder (through the placeholder attribute).
- Usage: Create placeholder DOM and return it. For example, amp-instagram uses this to create a placeholder dynamically by creating an amp-img placeholder instead of loading the iframe, leaving the iframe loading to layoutCallback.
- Warning: Only use amp-elements for creating placeholders that require external resource loading. This allows runtime to create this early but still defer the resource loading and management to AMP resources manager. Don't create or load heavyweight resources (e.g. iframe…).
- Example Usage: amp-instagram.
- Default: Does nothing.
- Vsync Context: Measure.
- Override: Rarely.
- Usage: Use to measure layouts for your element.
- Example Usage: amp-iframe
- Default: Does nothing.
- Vsync Context: Mutate
- Override: Almost always.
- Usage: Use this to actually render the final version of your element. If the element should load a video, this is where you load the video. This needs to return a promise that resolves when the element is considered "laid out" - usually this means load event has fired but can be different from element to element. Note that load events usually are fired very early so if there's another event that your element can listen to that have a better meaning of ready-ness, use that to resolve your promise instead - for example: amp-youtube uses the playerready event that the underlying YT Player iframe sends to resolve the layoutCallback promise.
- Default: Hide element's placeholder.
- Vsync Context: Mutate
- Override: Sometimes. If you'd like to override default behavior and not hide the placeholder when the element is considered first laid out. Sometimes you wanna control when to hide the placeholder.
- Example Usage: amp-anim
- Default: Does nothing.
- Vsync Context: Mutate
- Called: When you swipe away from a document in a viewer. Called on children of lightbox when you close a lightbox instance, called on carousel children when the slide is not the active slide. And possibly other places.
- Override: Sometimes. Most likely if you're building a player.
- Usage: Use to pause video, slideshow auto-advance...etc
- Example Usage: amp-video, amp-youtube
- Default: Does nothing.
- Vsync Context: Mutate
- Override: Sometimes.
- Usage: Use to restart the slideshow auto-advance.
- Note: This is not used widely yet because it's not possible to resume video playback for example on mobile.
- Default: Returns false.
- Vsync Context: Mutate
- Override: If your element doesn't provide a pausing mechanism, instead override this to unlayout the element when AMP tries to pause it.
- Return: True if you want unlayoutCallback to be called when paused.
- Usage Example: amp-brightcove
- Default: Does nothing.
- Vsync Context: Mutate
- Override: Sometimes.
- Usage: Use to remove and unload heavyweight resources like iframes, video, audio and others that your element has created.
- Return: True if your element need to re-layout.
- Usage Example: amp-iframe
You can write a stylesheet to style your element to provide a minimal visual appeal. Your element structure should account for whether you want users (publishers and developers using your element) to customize the default styling you're providing and allow for easy CSS classes and/or well-structure DOM elements.
Element styles are loaded when the element script itself is included in an AMP doc. You tell AMP which CSS belongs to this element when registering the element (see below).
Class names prefixed with i-amphtml
are considered private. Publishers
are not allowed to use them for customization (enforced by AMP validator).
Class names prefixed with amp-
are public css classes that can be customized
by publishers. All such classes should be documented in the component-specific
.md
file. All CSS classes in component stylesheets should be prefixed with
either i-amphtml-
or amp-
.
Once you have implemented your AMP element, you need to register it with
AMP; all AMP extensions are prefixed with amp-
. This is where you
tell AMP which class to use for this tag name and which CSS to load.
AMP.extension('amp-carousel', '0.1', (AMP) => {
AMP.registerElement('amp-carousel', CarouselSelector, CSS);
});
AMP provides a framework for elements to fire their own
events
to allow users of that element to listen and react to the events. For
example, amp-form extension fires a few events on <form> elements
like submit-success
. This allow publishers to listen to that event
and react to it, for example, by launching a lightbox to display a
message.
The other part of the event-system in AMP is actions. When listening to
an event on an element usually you'd like to trigger an action (possibly
on other elements). For example, in the example above, the publisher is
executing the open
action on lightbox
.
The syntax for using this on elements is as follow:
<form
on="submit-success:my-success-lightbox.open;submit-error:my-error-lightbox.open"
></form>
To fire events on your element use AMP's action service and the
.trigger
method.
actionServiceForDoc(doc.documentElement).trigger(
this.form_,
'submit-success',
null
);
And to expose actions use registerAction
method that your element
inherits from BaseElement
.
this.registerAction('close', this.close.bind(this));
Your element could also choose to override the activate
method
inherited from BaseElement that would define the default action for your
element. For example amp-lightbox overrides activate to define the open
default case.
Make sure your element documentation documents the events and actions it exposes.
AMP elements are usually discovered and scheduled by the AMP runtime automatically and managed through Resources. In some cases an AMP element might want to control and own when its sub-elements get scheduled and not leave that to the AMP runtime. An example to this is the <amp-carousel> component, where it wants to schedule preloading/pre-rendering or layouting of its cells based on the window the user is in.
AMP provides a way for an element to control this by setting the owner on the element you want to control. In the carousel example, the component loops over all its elements and sets itself as the owner of these elements. The AMP runtime will not manage scheduling layouting for elements that have owners.
this.cells_ = realChildElements(this.element);
this.cells_.forEach((cell) => {
Services.ownersForDoc(this.element).setOwner(cell, this.element);
cell.style.display = 'inline-block';
this.container_.appendChild(cell);
});
An element can then later call schedulePreload
or scheduleLayout
to
schedule preload or layout respectively. For example, <amp-carousel
type=slider> (Slider instance of amp-carousel) calls
schedulePreload
for the next/previous slide when the user moves
forward/backward in the slides and then calls scheduleLayout
for the
current slide when the user moves to it.
const owners = Services.ownersForDoc(this.element);
owners.scheduleLayout(this.element, newSlide);
this.setControlsState();
owners.schedulePause(this.element, oldSlide);
owners.schedulePreload(this.element, nextSlide);
It's important to understand that the parent/owner element is responsible for managing all of its children (except for placeholders, see below). This means you need to make sure your element updates whether the child is in viewport and when to schedule different phases for the element.
Your element should anticipate its sub-elements to nest some more amp-elements and schedule preload or layout for these as well, otherwise the element will never be preloaded or laid out. This is true to all nested amp-elements that are not placeholders. AMP runtime will schedule nested amp-elements that are placeholders.
<amp-carousel> ← Parent element
<amp-figure> ← Parent needs to schedule this element
<amp-img placeholder></amp-img> ← AMP will schedule this when amp-figure is scheduled
<amp-img></amp-img> ← Parent needs to schedule this element
<amp-fit-text></amp-fit-text> ← Parent needs to schedule this element
</amp-figure>
</amp-carousel>
One of AMP's features is that a document can be checked against validation rules to confirm it's valid AMP. When you implement your element, the AMP Validator needs to be updated to add rules for your element to keep documents using your element valid. Create your own rules by following the directions at Contributing Component Validator Rules.
Another enabling feature of instant-web in AMP is support for prerendering in a way that does not consume loads of data and does not waste too much of the user's device resources. AMP does this by strictly controlling resource loading and rendering.
If your extension is lightweight, it might be worth enabling pre-rendering of your elements so that users will be able to see it appear instantly when they click on an article.
Sometimes fully pre-rendering the element isn't an option because it is heavyweight. Your element might want to opt into creating a dynamic placeholder for itself (in case a placeholder wasn't provided by the developer/publisher who is using your element). This allows elements to display content as fast as possible and allow prerendering that placeholder. Learn more about placeholder elements.
NOTE: Make sure not to request external resources in the pre-render phase. Requests to the publisher's origin itself are OK. If in doubt, please flag this in review.
AMP will automatically call your element's
createPlaceholderCallback during
build step if it didn't detect a placeholder was provided. This allows
you to create your own placeholder. Here's an example of how
amp-instagram
element used this callback to create a dynamic
placeholder of an amp-img
element to avoid loading the heavyweight
instagram iframe embed during pre-rendering and instead loads just the
image directly from instagram media endpoint.
class AmpInstagram extends AMP.BaseElement {
// ...
/** @override */
createPlaceholderCallback() {
const placeholder = this.win.document.createElement('div');
placeholder.setAttribute('placeholder', '');
const image = this.win.document.createElement('amp-img');
// This is always the same URL that is actually used inside of the embed.
// This lets us avoid loading the image twice and make use of browser cache.
image.setAttribute(
'src',
'https://www.instagram.com/p/' +
encodeURIComponent(this.shortcode_) +
'/media/?size=l'
);
image.setAttribute('width', this.element.getAttribute('width'));
image.setAttribute('height', this.element.getAttribute('height'));
image.setAttribute('layout', 'responsive');
setStyles(image, {
'object-fit': 'cover',
});
const wrapper = this.element.ownerDocument.createElement('wrapper');
// This makes the non-iframe image appear in the exact same spot
// where it will be inside of the iframe.
setStyles(wrapper, {
'position': 'absolute',
'top': '48px',
'bottom': '48px',
'left': '8px',
'right': '8px',
});
wrapper.appendChild(image);
placeholder.appendChild(wrapper);
this.applyFillContent(image);
return placeholder;
}
// …
}
Important: One thing to keep in mind is that when you create a
placeholder, use the amp- provided elements when loading external
resources. This is most likely going to be amp-img
like in the
instagram case above. This still allows AMP resource manager to control
when these resources get loaded and rendered as oppose to using the
HTML-native img
tag which will be out of AMP resource management.
Consider showing a loading indicator if your element is expected to take
a long time to load (for example, loading a GIF, video or iframe). AMP
has a built-in mechanism to show a loading indicator simply by
listing your element so it's allowed to show it. You can do that inside the layout.js
file in the LOADING_ELEMENTS_ENUM
object.
export const LOADING_ELEMENTS_ENUM = {
...
AMP_YOUTUBE: 'AMP-YOUTUBE',
AMP_MY_ELEMENT: 'AMP-MY-ELEMENT',
}
To stay good to our promise of lowering resources usage especially on mobile, elements that create and load heavyweight resources (e.g. iframes, video, very large images, an expensive timer or computation...) need to be destroyed when they are no longer needed.
AMP signals to your element that it needs to do that with unlayoutCallback. AMP calls this when the document becomes inactive; like when the user swipes away from the document to another one or when they switch tabs.
This might be also be called in special cases like if your element is
used as an amp-carousel cell and it was swiped away to become outside
the viewport. This will only happen if your element sets
unlayoutOnPause
. Carousel by default only pauses the elements that
are outside its viewport.
Here's an example of how amp-instagram
destroys the iframe it has
embedded when unlayoutCallback
is called.
/** @override */
unlayoutCallback() {
if (this.iframe_) {
removeElement(this.iframe_);
this.iframe_ = null;
this.iframePromise_ = null;
setStyles(this.placeholderWrapper_, {
'display': '',
});
}
return true; // Call layoutCallback again.
}
Note if your element unlayoutCallback destroys the resources, it
probably wants to return true in order to signal to AMP the need to call
layoutCallback
again once the document is active. Otherwise your
element will never be re-laid out.
AMP provides multiple utilities to optimize many mutations and measuring for better performance. These include vsync service with a mutate and measure utility method that will synchronize all measuring happening in short period of time together and then do all the mutating in a requestAnimationFrame or similar cycles.
If your extension needs to load external resources (like an sdk) then you might need to add proper third party integration for it to work and use the proper third party iframe. Loading external resources is only allowed inside a 3p iframe which AMP serves on a different domain for security and performance reasons. Take a look at adding <amp-facebook> extension PR for examples of 3p integration.
Read about Inclusion of third party software, embeds and services into AMP.
For contrast, take a look at amp-instagram which does NOT require an SDK to be loaded in order to embed a post, instead it provides an iframe-based embedding allowing amp-instagram extension to use a normal iframe with no 3p integration needed, similarly, amp-youtube and others.
AMP defines different layouts that elements can choose whether or not to
support Your element needs to announce which layouts it supports through
overriding the isLayoutSupported(layout)
callback and returning true
if the element supports that layout. Read more about AMP Layout
System
and Layout
Types.
After understanding each layout type, if it makes sense, support all of
them. Otherwise choose what makes sense to your element. A popular
support choice is to support size-defined layouts (Fixed, Fixed Height,
Responsive and Fill) through using the utility isLayoutSizeDefined
in layout.js
.
For example, amp-pixel
only supports fixed layout.
class AmpPixel extends BaseElement {
/** @override */
isLayoutSupported(layout) {
return layout == Layout.FIXED;
}
}
While amp-carousel
supports all size-defined layouts.
class AmpSlides extends AMP.BaseElement {
/** @override */
isLayoutSupported(layout) {
return isLayoutSizeDefined(layout);
}
}
Most newly created elements are initially launched as experiments. This allows people to experiment with using the new element and provide the author(s) with feedback. It also provides the AMP Team with the opportunity to monitor for any potential errors. This is especially required if the validator hasn't been updated yet to allow your newly created extension, otherwise people using it in production will invalidate all their AMP documents.
Add your extension as an experiment in the
amphtml/tools/experiments
file by adding a record for your extension
in EXPERIMENTS variable.
/** @const {!Array<!ExperimentDef>} */
const EXPERIMENTS = [
// ...
{
id: 'amp-my-element',
name: 'AMP My Element',
spec:
'https://github.com/ampproject/amphtml/blob/main/extensions/' +
'amp-my-element/amp-my-element.md',
cleanupIssue: 'https://github.com/ampproject/amphtml/issues/XXXYYY',
},
// ...
];
And then protecting your code with a check isExperimentOn(win, 'amp-my-element')
and only execute your code when it is on.
import {isExperimentOn} from '../../../src/experiments';
import {userAssert} from '../../../src/log';
/** @const */
const EXPERIMENT = 'amp-my-element';
/** @const */
const TAG = 'amp-my-element';
Class AmpMyElement extends AMP.BaseElement {
/** @param {!AmpElement} element */
constructor(element) {
super(element);
// declare instance variables with type annotations.
}
/** @override */
isLayoutSupported(layout) {
return layout == LAYOUT.FIXED;
}
/** @override */
buildCallback() {
userAssert(isExperimentOn(this.win, 'amp-my-element'),
`Experiment ${EXPERIMENT} is not turned on.`);
// get attributes, assertions of values, assign instance variables.
// build lightweight dom and append to this.element.
}
/** @override */
layoutCallback() {
userAssert(isExperimentOn(this.win, 'amp-my-element'),
`Experiment ${EXPERIMENT} is not turned on.`);
// actually load your resource or render more expensive resources.
}
}
AMP.extension('amp-my-element', '0.1', AMP => {
AMP.registerElement('amp-my-element', AmpMyElement, CSS);
});
Users wanting to experiment with your element can then go to the experiments page and enable your experiment.
If you are testing on your localhost, use the command AMP.toggleExperiment(id, true/false)
to enable the experiment.
File a github issue to cleanup your experiment. Assign it to yourself as a reminder to remove your experiment and code checks. Removal of your experiment happens after the extension has been thoroughly tested and all issues have been addressed.
Create a .md file that serves as the main documentation for your element. This document should include:
- Summary table
- Overview
- How to use it including code snippets and images
- Examples
- Attributes to specify (optional and required)
- Validation
For samples of element documentation, see: amp-list, amp-instagram, amp-carousel
This greatly helps users to understand and demonstrate how
your element works, and provides an easy start-point for them to
experiment with it. This is basically where you actually build an AMP
HTML document and use your element in it by creating a file in the
examples/
directory, usually with the my-element.amp.html
file
name. Browse that directory to see examples for other elements and
extensions.
Also consider contributing an example to amp.dev on GitHub.
In order for your element to build correctly you would need to make few
changes to build-system/compile/bundles.config.extensions.json
to tell it about your
extension, its files and its examples. You will need to add an entry in the top-level array.
exports.extensionBundles = [
...
{name: 'amp-kaltura-player', version: '0.1'},
{name: 'amp-carousel', version: '0.1', options: {hasCss: true}},
...
];
AMP runtime is currently in v0 major version. Extensions versions are maintained separately. If your changes to your non-experimental extension makes breaking changes that are not backward compatible you should version your extension. This would usually be by creating a 0.2 directory next to your 0.1.
If your extension is still in experiments breaking changes usually are fine so you can just update the same version.
Make sure you write good coverage for your code. We require unit tests for all checked in code. We use the following frameworks for testing:
For faster testing during development, consider using --files argument to only run your extensions' tests.
$ amp unit --files=extensions/amp-my-element/0.1/test/test-amp-my-element.js --watch
We use Closure Compiler to perform type checking. Please see Annotating JavaScript for the Closure Compiler and existing AMP code for examples of how to add type annotations to your code. The following command should be run to ensure no type violations are introduced by your extension.
$ amp check-types
- Adding new ad-provider
- Adding embeds that's not iframe-based (requires JS SDK)
- Adding iframe based embeds
- Adding non-visual elements
- Adding general UI components
- Implementing a video player.