SkillAgentSearch skills...

Ocean

Web component server-side rendering

Install / Use

/learn @matthewp/Ocean
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

🌊 Ocean

Web component HTML rendering that includes:

  • Rendering to Declarative Shadow DOM, requiring no JavaScript in the client.
  • Automatic inclusion of the Declarative Shadow DOM polyfill for browsers without support.
  • Streaming HTML responses.
  • Compatibility with the most popular web component libraries (see a compatibility list below).
  • Lazy partial hydration via special attributes: hydrate on page load, CPU idle, element visibility, or media queries. Or create your own hydrator.

Table of Contents

Overview

An ocean is an environment for rendering web component code. It provides an html function that looks like the ones you're used to from libraries like uhtml and Lit. Instead of creating reactive DOM in the client like those libraries, Ocean's html returns an async iterator that will stream out HTML strings.

Ocean is somewhat low-level and is meant to be used with a higher-level framework. Typical usage looks like this:

import 'https://cdn.spooky.click/ocean/1.3.1/shim.js?global';
import { Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';

const { HTMLElement, customElements, document } = globalThis;

class AppRoot extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  connectedCallback() {
    let div = document.createElement('div');
    div.textContent = `This is an app!`;
    this.shadowRoot.append(div);
  }
}

customElements.define('app-root', AppRoot);

const { html } = new Ocean({
  document,
  polyfillURL: '/webcomponents/declarative-shadow-dom.js'
});

let iterator = html`
  <!doctype html>
  <html lang="en">
  <title>My app</title>

  <app-root></app-root>
`;

let code = '';
for await(let chunk of iterator) {
  code += chunk;
}
console.log(chunk); // HTML string

The above will generate the following HTML:

<!doctype html>
<html lang="en">
<title>My app</title>

<script type="module">const o=(new DOMParser).parseFromString('<p><template shadowroot="open"></template></p>',"text/html",{includeShadowRoots:!0}).querySelector("p");o&&o.shadowRoot||async function(){const{hydrateShadowRoots:o}=await import("/webcomponents/declarative-shadow-dom.js");o(document.body)}()</script>
<app-root>
  <template shadowroot="open">
    <div>This is an app!</div>
  </template>
</app-root>

Modules

Ocean comes with its main module and a DOM shim for compatible with custom element code.

Main module

The main module for Ocean is available in two forms: bundled and unbundled.

  • If you are using Ocean in a browser context, such as a service worker, use the bundled version.
  • If you are using Ocean in Deno, use the unbundled version.

Unbundled

import { Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';

Bundled

import { Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.bundle.js';

DOM shim

Ocean's DOM shim is backed by linkedom, a fast DOM layer. The shim also bridges compatibility with popular web component libraries.

It's important to import the DOM shim as one of the first imports in your app.

import 'https://cdn.spooky.click/ocean/1.3.1/shim.js?global';

Notice that this includes in the ?global query parameter. This makes the shim available on globals; you get document, customElements, and other commonly used global variables.

If you do not want to shim the global environment you can omit the ?global query parameter and instead get the globals yourself from the symbol Symbol.for('dom-shim.defaultView'). This is advanced usage.

import 'https://cdn.spooky.click/ocean/1.3.1/shim.js';

const root = globalThis[Symbol.for('dom-shim.defaultView')];
const { HTMLElement, customElements, document } = root;

Hydration

Partial hydration is the practice of only hydrating (via running client JavaScript) components that are needed for interactivity. Ocean does not automatically add scripts for components by default. However Ocean does support both full and partial hydration. This means you can omit the component script tags from your HTML and Ocean will automatically add them for you.

In order to add script tags you have to provide Ocean a map of tag names to URLs to load. You do this through the elements Map that is returned from the constructor.

let { html, elements } = new Ocean({
  document
});

elements.set('app-sidebar', '/elements/app-sidebar.js');

Note: Ocean only adds script tags for elements that are server rendered. If you are not server rendering an element you will need to add the appropriate script tags yourself.

Full hydration

Full hydration means added script tags to the <head> for any components that are server rendered. You can enable full hydration by passing this in the constructor:

let { html, elements } = new Ocean({
  document,
  hydration: 'full'
});

elements.set('app-sidebar', '/elements/app-sidebar.js');

customElements.define('app-sidebar', class extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  connectedCallback() {
    let div = document.createElement('div');
    div.textContent = `My sidebar...`;
    this.shadowRoot.append(div);
  }
});

Then when you render this element, it will include the script tags:

let iterator = html`
  <!doctype html>
  <html lang="en">
  <title>My app</title>

  <app-sidebar></app-sidebar>
`;

let out = '';
for(let chunk of iterator) {
  out += chunk;
}

Will produce this HTML:

<!doctype html>
<html lang="en">
<title>My app</title>
<script type="module" src="/elements/app-sidebar.js"></script>

<app-sidebar>
  <template shadowroot="open">
    <div>My sidebar...</div>
  </template>
</app-sidebar>

Partial hydration

By default Ocean uses partial hydration. In partial hydration script tags are only added when you explicitly tell Ocean to hydration an element. This means that by default elements will be rendered to HTML only, and never iteractive on the client.

This allows you to use the web component libraries you love both to produce static HTML and for interactive content.

To declare an element to be hydrated, use the ocean-hydrate attribute on any element. The value should be one of:

  • load: Hydrate when the page loads. Ocean will add a <script type="module"> tag for the element's script.
  • idle: Hydrate when the CPU becomes idle. Ocean will add an inline script that waits for requestIdleCallback and then loads the element's script.
  • media: Hydrates on a matching media query. This allows you to have some elements which only hydrate for certain screen sizes. Use the ocean-query attribute to specify the media query.
  • visible: Hydrate when the element becomes visible. This is useful for elements which are shown further down the page. Ocean will add an inline script that uses Intersection Observer to determine when the element is visible and then loads the script.

Using one of these hydrators looks like:

let iterator = html`
  <!doctype html>
  <html lang="en">
  <title>My site</title>

  <app-sidebar ocean-hydrate="idle"></app-sidebar>
`;

Hydrator options

You can specify which hydrators you want to use by providing the hydrators option to Ocean. Each of the default hydrators are included by default, but can also be imported.

import {
  HydrateIdle,
  HydrateLoad,
  HydrateMedia,
  HydrateVisible,
  Ocean
} from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';
Load

To specify to hydrate on load, pass load into the ocean-hydrate attr:

let { html } = new Ocean({ document });

let iterator = html`
  <!doctype html>
  <html lang="en">
  <title>My site</title>

  <app-sidebar ocean-hydrate="load"></app-sidebar>
`;

HydrateLoad does not take any options because it only adds a script tag to the head. You can create an instance by calling new on it:

import { HydrateLoad, Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';

let { html } = new Ocean({
  document,
  hydrators: [
    new HydrateLoad()
  ]
});
Idle

To specify to hydrate on idle, pass idle into the ocean-hydrate attr:

let { html } = new Ocean({ document });

let iterator = html`
  <!doctype html>
  <html lang="en">
  <title>My site</title>

  <app-sidebar ocean-hydrate="idle"></app-sidebar>
`;

HydrateIdle uses a custom element to perform hydration when the CPU is idle. By default that custom element name is ocean-hydrate-idle. You can specify a different custom element name by passing it into the constructor.

import { HydrateIdle, Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';

let { html } = new Ocean({
  document,
  hydrators: [
    new HydrateIdle('my-app-hydrate-idle')
  ]
});
Media

To hydrate on a media query, pass media into the ocean-hydrate attr, and also provide a ocean-query attr with the media query to use:

let { html } = new Ocean({ document });

let iterator = html`
  <!doctype html>
  <html lang="en">
  <title>My site</title>

  <app-sidebar ocean-hydrate="media" ocean-query="(max-width: 700px)"></app-sidebar>
`

HydrateMedia uses the custom element ocean-hydrate-media to hy

View on GitHub
GitHub Stars187
CategoryDevelopment
Updated1mo ago
Forks4

Languages

JavaScript

Security Score

100/100

Audited on Feb 20, 2026

No findings