Life Cycle of Plasmo CSUI
Plasmo's CSUI orchestrates a lifecycle dedicated to mounting and unmounting your React, Vue, or Svelte components in a content script. Although each UI library/framework has a slightly different mounting API, the top-level lifecycle is largely the same:
- Get an
Anchor
- Create or locate a
Root Container
- Render the component onto the
Root Container
Terminologies
Term | Description |
---|---|
Anchor | Tell CSUI how and where to mount your component |
Anchor-getter | Tell CSUI how to find your anchor(s) |
Overlay | Mount your component on a top-level (max z-index) overlay element |
Inline | Mount your component into the webpage's DOM, next to a target element |
Root Container | A ShadowDOM element created by CSUI to isolate your component |
Renderer | The top-level life-cycle runner (it does everything) |
Anchor
A Plasmo CSUI anchor is defined by the following type:
exporttypePlasmoCSUIAnchor= { type:"overlay"|"inline" element:Element}
By default, the CSUI lifecycle creates an overlay anchor using thedocument.body
element:
{ type:"overlay", element:document.body}
If any anchor-getter function is defined and exported, the CSUI lifecycle will use the returned element and the relevant anchor type instead. Since the anchor-getter functions can be async, you also have the power to controlwhen Plasmo mounts your component. For example, you can wait for a specific element to appear on the page before mounting your component.
The anchor is passed down to the CSUI via the anchor props. You can access it as follow:
importtype { PlasmoCSUIProps }from"plasmo"constAnchorTypePrinter:FC<PlasmoCSUIProps>= ({ anchor })=> {return <span>{anchor.type}</span>}exportdefault AnchorTypePrinter
Overlay
Overlay anchors spawnCSUI Overlay Containers
which are batch-mounted onto a singleRoot Container
element per CSUI. TheOverlay Containers
are absolutely positioned relative to each anchor's element with maxed out z-index. Then, your exportedCSUI Component
is mounted onto eachOverlay Container
:
To specify a single overlay anchor, export agetOverlayAnchor
function:
importtype { PlasmoGetOverlayAnchor }from"plasmo"exportconstgetOverlayAnchor:PlasmoGetOverlayAnchor=async ()=>document.querySelector("#pricing")
To specify a list of overlay anchors, export agetOverlayAnchorList
function:
importtype { PlasmoGetOverlayAnchorList }from"plasmo"exportconstgetOverlayAnchorList:PlasmoGetOverlayAnchorList=async ()=>document.querySelectorAll("a")
getOverlayAnchorList
does not cover dynamic case at the moment. For example,if new anchors are added to the web page after the initial rendering, the CSUIlifecycle will not be able to detect it. PR is welcome to improve thisfeature!
Update Position
The defaultOverlay Container
listens to the window scroll event to align itself with the anchor element. You can customize how theOverlay Container
refreshes its absolute positioning by exporting awatchOverlayAnchor
function. The example below refreshes the position every 8472ms:
importtype { PlasmoWatchOverlayAnchor }from"plasmo"exportconstwatchOverlayAnchor:PlasmoWatchOverlayAnchor= ( updatePosition)=> {constinterval=setInterval(()=> {updatePosition() },8472)// Clear the interval when unmountedreturn ()=> {clearInterval(interval) }}
Checkwith-content-scripts-ui/contents/plasmo-overlay-watch.tsx (opens in a new tab) for an example.
Inline
Inline anchor embeds yourCSUI Component
directly into the web page. Each anchor spawns aRoot Container
appended next to its target element. Within eachRoot Container
, anInline Container
is created which is then used to mount the exportedCSUI Component
:
To specify a single inline anchor, export agetInlineAnchor
function:
importtype { PlasmoGetInlineAnchor }from"plasmo"exportconstgetInlineAnchor:PlasmoGetInlineAnchor=async ()=>document.querySelector("#pricing")
To specify single inline anchor with insert position:
importtype { PlasmoGetInlineAnchor }from"plasmo"exportconstgetInlineAnchor:PlasmoGetInlineAnchor=async ()=> ({ element:document.querySelector("#pricing"), insertPosition:"afterend"})
To specify a list of inline anchors, export agetInlineAnchorList
function:
importtype { PlasmoGetInlineAnchorList }from"plasmo"exportconstgetInlineAnchorList:PlasmoGetInlineAnchorList=async ()=>document.querySelectorAll("a")
To specify a list of inline anchors with insert position:
importtype { PlasmoGetInlineAnchorList }from"plasmo"exportconstgetInlineAnchorList:PlasmoGetInlineAnchorList=async ()=> {constanchors=document.querySelectorAll("a")returnArray.from(anchors).map((element)=> ({ element, insertPosition:"afterend" }))}
Checkwith-content-scripts-ui/contents/plasmo-inline.tsx (opens in a new tab) for an example.
Root Container
TheRoot Container
is where yourCSUI Component
is mounted. The built-inRoot Container
is a ShadowDOM element with theplasmo-csui
custom tag. This allows you to style theRoot Container
and their exported components without being impacted by the web page's styles.
Custom DOM Mounting
TheRoot Container
creates ashadowHost
which gets injected into the web page's DOM tree. By default, Plasmo injects theshadowHost
after the element for an inline anchor, and before thedocument.body
for an overlay anchor. To customize this behavior, export amountShadowHost
function:
importtype { PlasmoMountShadowHost }from"plasmo"exportconstmountShadowHost:PlasmoMountShadowHost= ({ shadowHost, anchor, mountState})=> {anchor.element.appendChild(shadowHost)mountState.observer.disconnect()// OPTIONAL DEMO: stop the observer as needed}
Closed Shadow Root
By default, the shadow root is "open," allowing anyone (developer and extension user) to inspect the hierarchy of the ShadowDOM. To override this behavior, export acreateShadowRoot
function:
importtype { PlasmoCreateShadowRoot }from"plasmo"exportconstcreateShadowRoot:PlasmoCreateShadowRoot= (shadowHost)=>shadowHost.attachShadow({ mode:"closed" })
Custom Styles
The built-in ShadowDOM provides a convenient mechanism for extension developers to safely style their components by exporting agetStyle
function that returns anHTML style element (opens in a new tab).
For further guidance on styling CSUI, please readStyling Plasmo CSUI.
Custom Root Container
Sometimes, you'll want to completely replace Plasmo's Shadow DOM container implementation to fit your needs. For example, you might want to piggyback on an element within the web page itself instead of creating a new DOM element. To do so, export agetRootContainer
function:
importtype { PlasmoGetRootContainer }from"plasmo"exportconstgetRootContainer= ()=>document.getElementById("itero")
Some reasons you'd want to do this:
- The extension needs toabsorb the styling of the host webpage (opens in a new tab)
- The extension needs to mount the component directly into the webpage instead of using a shadow DOM
- The extension needs to use aniframe (opens in a new tab), instead
If you export agetRootContainer
function, any function that extends thebuilt-in ShadowDOM such asgetStyle
orgetShadowHostId
will be ignored.Call those functions within your customgetRootContainer
logic as needed!
Checkwith-content-scripts-ui (opens in a new tab) for an example.
Renderer
TheRenderer
is in charge of observing the website's DOM to detect the presence of eachRoot Container
and tracking the linking between each anchor's element and itsRoot Container
. Once a stableRoot Container
is determined, theRenderer
mounts the exportedCSUI Component
into theRoot Container
using either anInline Container
or anOverlay Container
, depending on the type of theAnchor
.
Detecting and Optimizing Root Container Removal
When a webpage changes its DOM structure, theRoot Container
might be removed. For example, given an email client filled with inbox items, and a CSUI injected inline next to each item. When an item is deleted, the root container will be removed as well.
To detectRoot Container
removal, theCSUI Renderer
compares each mounted container's root against thewindow.document
object. This check can be optimized to O(1) by exporting agetShadowHostId
function:
importtype { PlasmoGetShadowHostId }from"plasmo"exportconstgetShadowHostId:PlasmoGetShadowHostId= ()=>`adonais`
The function also allows developers to customize the id for each anchor found:
importtype { PlasmoGetShadowHostId }from"plasmo"exportconstgetShadowHostId:PlasmoGetShadowHostId= ({ element })=>element.getAttribute("data-custom-id")+`-pollax-iv`
Custom Renderer
Developers may export arender
function to override the default renderer. You might need this ability to:
- Provide a custom
Inline container
orOverlay container
- Customize the mounting logic
- Provide a custom
MutationObserver
For example, to use an existing element as a custom container:
importtype { PlasmoRender }from"plasmo"import { CustomContainer }from"~components/custom-container"constEngageOverlay= ()=> <span>ENGAGE</span>// This function overrides the default `createRootContainer`exportconstgetRootContainer= ()=>newPromise((resolve)=> {constcheckInterval=setInterval(()=> {constrootContainer=document.getElementById("itero")if (rootContainer) {clearInterval(checkInterval)resolve(rootContainer) } },137) })exportconstrender:PlasmoRender=async ({ anchor,// the observed anchor, OR document.body. createRootContainer// This creates the default root container})=> {constrootContainer=awaitcreateRootContainer()constroot=createRoot(rootContainer)// Any rootroot.render( <CustomContainer> <EngageOverlay /> </CustomContainer> )}
How to dynamically create a custom container:
importtype { PlasmoRender }from"plasmo"import { CustomContainer }from"~components/custom-container"constEngageOverlay= ()=> <span>ENGAGE</span>// This function overrides the default `createRootContainer`exportconstgetRootContainer= ({ anchor, mountState })=>newPromise((resolve)=> {constcheckInterval=setInterval(()=> {let { element, insertPosition }= anchorif (element) {constrootContainer=document.createElement("div")mountState.hostSet.add(rootContainer)mountState.hostMap.set(rootContainer, anchor)element.insertAdjacentElement(insertPosition, rootContainer)clearInterval(checkInterval)resolve(rootContainer) } },137) })exportconstrender:PlasmoRender=async ({ anchor,// the observed anchor, OR document.body. createRootContainer// This creates the default root container})=> {constrootContainer=awaitcreateRootContainer(anchor)constroot=createRoot(rootContainer)// Any rootroot.render( <CustomContainer> <EngageOverlay /> </CustomContainer> )}
To utilize the built-inInline Container
orOverlay Container
:
importtype { PlasmoRender }from"plasmo"constAnchorOverlay= ({ anchor })=> <span>{anchor.innerText}</span>exportconstrender:PlasmoRender=async ( { anchor,// the observed anchor, OR document.body. createRootContainer// This creates the default root container }, _, OverlayCSUIContainer)=> {constrootContainer=awaitcreateRootContainer()constroot=createRoot(rootContainer)// Any rootroot.render(// You must pass down an anchor to mount the default container. Here we pass the default one <OverlayCSUIContaineranchor={anchor}> <AnchorOverlayanchor={anchor} /> </OverlayCSUIContainer> )}
If you need to customize the MutationObserver, do not export an anchor-getter function. Otherwise, the built-in MutationObserver will still be spawned.