{"id":13630051,"url":"https://github.com/matthewp/ocean","last_synced_at":"2025-10-25T17:43:21.815Z","repository":{"id":46137806,"uuid":"389800993","full_name":"matthewp/ocean","owner":"matthewp","description":"Web component server-side rendering","archived":false,"fork":false,"pushed_at":"2021-10-06T12:06:51.000Z","size":335,"stargazers_count":181,"open_issues_count":3,"forks_count":3,"subscribers_count":9,"default_branch":"main","last_synced_at":"2025-01-03T02:24:05.707Z","etag":null,"topics":["partial-hydration","ssr","web-components"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-2-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/matthewp.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-07-27T00:09:08.000Z","updated_at":"2024-10-24T23:12:00.000Z","dependencies_parsed_at":"2022-08-29T16:41:52.769Z","dependency_job_id":null,"html_url":"https://github.com/matthewp/ocean","commit_stats":null,"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/matthewp%2Focean","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/matthewp%2Focean/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/matthewp%2Focean/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/matthewp%2Focean/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/matthewp","download_url":"https://codeload.github.com/matthewp/ocean/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":234195442,"owners_count":18794303,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["partial-hydration","ssr","web-components"],"created_at":"2024-08-01T22:01:28.533Z","updated_at":"2025-09-25T13:31:22.051Z","avatar_url":"https://github.com/matthewp.png","language":"JavaScript","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"readme":"# 🌊 Ocean\n\nWeb component HTML rendering that includes:\n\n* Rendering to [Declarative Shadow DOM](https://web.dev/declarative-shadow-dom/), requiring no JavaScript in the client.\n* Automatic inclusion of the Declarative Shadow DOM polyfill for browsers without support.\n* Streaming HTML responses.\n* Compatibility with the most popular web component libraries (see a compatibility list below).\n* Lazy [partial hydration](https://www.jameshill.dev/articles/partial-hydration/) via special attributes: hydrate on page load, CPU idle, element visibility, or media queries. Or create your own hydrator.\n\n---\n\n__Table of Contents__\n\n* __[Overview](#overview)__\n* __[Modules](#modules)__\n  * __[Main module](#main-module)__\n  * __[DOM shim](#dom-shim)__\n* __[Hydration](#hydration)__\n  * __[Full hydration](#full-hydration)__\n  * __[Partial hydration](#partial-hydration)__\n* __[Relative links](#relative-links)__\n* __[Plugins](#plugins)__\n* __[Compatibility](#compatibility)__\n\n## Overview\n\nAn *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](https://github.com/WebReflection/uhtml) and [Lit](https://lit.dev/). Instead of creating reactive DOM in the client like those libraries, Ocean's `html` returns an *async iterator* that will stream out HTML strings.\n\nOcean is somewhat low-level and is meant to be used with a higher-level framework. Typical usage looks like this:\n\n```js\nimport 'https://cdn.spooky.click/ocean/1.3.1/shim.js?global';\nimport { Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';\n\nconst { HTMLElement, customElements, document } = globalThis;\n\nclass AppRoot extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: 'open' });\n  }\n  connectedCallback() {\n    let div = document.createElement('div');\n    div.textContent = `This is an app!`;\n    this.shadowRoot.append(div);\n  }\n}\n\ncustomElements.define('app-root', AppRoot);\n\nconst { html } = new Ocean({\n  document,\n  polyfillURL: '/webcomponents/declarative-shadow-dom.js'\n});\n\nlet iterator = html`\n  \u003c!doctype html\u003e\n  \u003chtml lang=\"en\"\u003e\n  \u003ctitle\u003eMy app\u003c/title\u003e\n\n  \u003capp-root\u003e\u003c/app-root\u003e\n`;\n\nlet code = '';\nfor await(let chunk of iterator) {\n  code += chunk;\n}\nconsole.log(chunk); // HTML string\n```\n\nThe above will generate the following HTML:\n\n```html\n\u003c!doctype html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003ctitle\u003eMy app\u003c/title\u003e\n\n\u003cscript type=\"module\"\u003econst o=(new DOMParser).parseFromString('\u003cp\u003e\u003ctemplate shadowroot=\"open\"\u003e\u003c/template\u003e\u003c/p\u003e',\"text/html\",{includeShadowRoots:!0}).querySelector(\"p\");o\u0026\u0026o.shadowRoot||async function(){const{hydrateShadowRoots:o}=await import(\"/webcomponents/declarative-shadow-dom.js\");o(document.body)}()\u003c/script\u003e\n\u003capp-root\u003e\n  \u003ctemplate shadowroot=\"open\"\u003e\n    \u003cdiv\u003eThis is an app!\u003c/div\u003e\n  \u003c/template\u003e\n\u003c/app-root\u003e\n```\n\n## Modules\n\nOcean comes with its main module and a DOM shim for compatible with custom element code.\n\n### Main module\n\nThe main module for Ocean is available in two forms: bundled and unbundled.\n\n* If you are using Ocean in a browser context, such as a service worker, use the bundled version.\n* If you are using Ocean in [Deno](https://deno.land/), use the unbundled version.\n\n#### Unbundled\n\n```js\nimport { Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';\n```\n\n#### Bundled\n\n```js\nimport { Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.bundle.js';\n```\n\n### DOM shim\n\nOcean's DOM shim is backed by [linkedom](https://github.com/WebReflection/linkedom), a fast DOM layer. The shim also bridges compatibility with popular web component libraries.\n\nIt's important to import the DOM shim as one of the first imports in your app.\n\n```js\nimport 'https://cdn.spooky.click/ocean/1.3.1/shim.js?global';\n```\n\nNotice 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.\n\nIf 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.\n\n```js\nimport 'https://cdn.spooky.click/ocean/1.3.1/shim.js';\n\nconst root = globalThis[Symbol.for('dom-shim.defaultView')];\nconst { HTMLElement, customElements, document } = root;\n```\n\n## Hydration\n\nPartial 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.\n\nIn 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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) that is returned from the constructor.\n\n```js\nlet { html, elements } = new Ocean({\n  document\n});\n\nelements.set('app-sidebar', '/elements/app-sidebar.js');\n```\n\n\u003e *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.\n\n### Full hydration\n\nFull hydration means added script tags to the `\u003chead\u003e` for any components that are server rendered. You can enable full hydration by passing this in the constructor:\n\n```js\nlet { html, elements } = new Ocean({\n  document,\n  hydration: 'full'\n});\n\nelements.set('app-sidebar', '/elements/app-sidebar.js');\n\ncustomElements.define('app-sidebar', class extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: 'open' });\n  }\n  connectedCallback() {\n    let div = document.createElement('div');\n    div.textContent = `My sidebar...`;\n    this.shadowRoot.append(div);\n  }\n});\n```\n\nThen when you render this element, it will include the script tags:\n\n```js\nlet iterator = html`\n  \u003c!doctype html\u003e\n  \u003chtml lang=\"en\"\u003e\n  \u003ctitle\u003eMy app\u003c/title\u003e\n\n  \u003capp-sidebar\u003e\u003c/app-sidebar\u003e\n`;\n\nlet out = '';\nfor(let chunk of iterator) {\n  out += chunk;\n}\n```\n\nWill produce this HTML:\n\n```html\n\u003c!doctype html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003ctitle\u003eMy app\u003c/title\u003e\n\u003cscript type=\"module\" src=\"/elements/app-sidebar.js\"\u003e\u003c/script\u003e\n\n\u003capp-sidebar\u003e\n  \u003ctemplate shadowroot=\"open\"\u003e\n    \u003cdiv\u003eMy sidebar...\u003c/div\u003e\n  \u003c/template\u003e\n\u003c/app-sidebar\u003e\n```\n\n### Partial hydration\n\nBy 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.\n\nThis allows you to use the web component libraries you love both to produce static HTML and for interactive content.\n\nTo declare an element to be hydrated, use the `ocean-hydrate` attribute on any element. The value should be one of:\n\n* __load__: Hydrate when the page loads. Ocean will add a `\u003cscript type=\"module\"\u003e` tag for the element's script.\n* __idle__: Hydrate when the CPU becomes idle. Ocean will add an inline script that waits for `requestIdleCallback` and then loads the element's script.\n* __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.\n* __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](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to determine when the element is visible and then loads the script.\n\nUsing one of these hydrators looks like:\n\n```js\nlet iterator = html`\n  \u003c!doctype html\u003e\n  \u003chtml lang=\"en\"\u003e\n  \u003ctitle\u003eMy site\u003c/title\u003e\n\n  \u003capp-sidebar ocean-hydrate=\"idle\"\u003e\u003c/app-sidebar\u003e\n`;\n```\n\n#### Hydrator options\n\nYou 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.\n\n```js\nimport {\n  HydrateIdle,\n  HydrateLoad,\n  HydrateMedia,\n  HydrateVisible,\n  Ocean\n} from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';\n```\n\n##### Load\n\nTo specify to hydrate on load, pass `load` into the `ocean-hydrate` attr:\n\n```js\nlet { html } = new Ocean({ document });\n\nlet iterator = html`\n  \u003c!doctype html\u003e\n  \u003chtml lang=\"en\"\u003e\n  \u003ctitle\u003eMy site\u003c/title\u003e\n\n  \u003capp-sidebar ocean-hydrate=\"load\"\u003e\u003c/app-sidebar\u003e\n`;\n```\n\n__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:\n\n```js\nimport { HydrateLoad, Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';\n\nlet { html } = new Ocean({\n  document,\n  hydrators: [\n    new HydrateLoad()\n  ]\n});\n```\n\n##### Idle\n\nTo specify to hydrate on idle, pass `idle` into the `ocean-hydrate` attr:\n\n```js\nlet { html } = new Ocean({ document });\n\nlet iterator = html`\n  \u003c!doctype html\u003e\n  \u003chtml lang=\"en\"\u003e\n  \u003ctitle\u003eMy site\u003c/title\u003e\n\n  \u003capp-sidebar ocean-hydrate=\"idle\"\u003e\u003c/app-sidebar\u003e\n`;\n```\n\n__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.\n\n```js\nimport { HydrateIdle, Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';\n\nlet { html } = new Ocean({\n  document,\n  hydrators: [\n    new HydrateIdle('my-app-hydrate-idle')\n  ]\n});\n```\n\n##### Media\n\nTo 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:\n\n```js\nlet { html } = new Ocean({ document });\n\nlet iterator = html`\n  \u003c!doctype html\u003e\n  \u003chtml lang=\"en\"\u003e\n  \u003ctitle\u003eMy site\u003c/title\u003e\n\n  \u003capp-sidebar ocean-hydrate=\"media\" ocean-query=\"(max-width: 700px)\"\u003e\u003c/app-sidebar\u003e\n`\n```\n\n__HydrateMedia__ uses the custom element `ocean-hydrate-media` to hydrate your custom element. You can customize this, and also the attribute used for the query by passing those arguments into the constructor:\n\n```js\nimport { HydrateMedia, Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';\n\nlet { html } = new Ocean({\n  document,\n  hydrators: [\n    new HydrateMedia('my-app-hydrate-media', 'app-query')\n  ]\n});\n\nlet iterator = html`\n  \u003c!doctype html\u003e\n  \u003chtml lang=\"en\"\u003e\n  \u003ctitle\u003eMy site\u003c/title\u003e\n\n  \u003capp-sidebar ocean-hydrate=\"media\" app-query=\"(max-width: 700px)\"\u003e\u003c/app-sidebar\u003e\n`;\n```\n\n##### Visible\n\nTo specify to hydrate on element visibility, pass `visible` into the `ocean-hydrate` attr:\n\n```js\nlet { html } = new Ocean({ document });\n\nlet iterator = html`\n  \u003c!doctype html\u003e\n  \u003chtml lang=\"en\"\u003e\n  \u003ctitle\u003eMy site\u003c/title\u003e\n\n  \u003capp-sidebar ocean-hydrate=\"visible\"\u003e\u003c/app-sidebar\u003e\n`;\n```\n\n__HydrateVisible__ uses the custom element `ocean-hydrate-visible` to track when your element is visible. You can customize this custom element tag name by passing in something else into the constructor:\n\n```js\nimport { HydrateVisible, Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';\n\nlet { html } = new Ocean({\n  document,\n  hydrators: [\n    new HydrateVisible('my-app-hydrate-visible')\n  ]\n});\n```\n\n#### Custom hydrator\n\nA hydrator is an object that specifies how to hydrate the element. You can create a custom hydrator and pass it to the `hydrators` option.\n\nThe following is a hydrator that hydrates whenever the element is clicked.\n\n```js\nconst clickHydrator = {\n  condition: 'click',\n  tagName: 'my-click-hydrator',\n  renderMultiple: true,\n  script() {\n    return /* js */ `customElements.define('${this.tagName}', class extends HTMLElement {\n  connectedCallback() {\n    let el = this.previousElementSibling;\n    let src = this.getAttribute('src');\n    el.addEventListener('click', () =\u003e import(src), { once: true });\n  }\n})`;\n  }\n};\n\nlet { html } = new Ocean({\n  document,\n  hydrators: [\n    clickHydrator\n  ]\n})\n```\n\nWhich you would use like so:\n\n```js\nlet iterator = html`\n  \u003c!doctype html\u003e\n  \u003chtml lang=\"en\"\u003e\n  \u003ctitle\u003eMy site\u003c/title\u003e\n\n  \u003capp-sidebar ocean-hydrate=\"click\"\u003e\u003c/app-sidebar\u003e\n`;\n```\n\nThe properties of a hydrator are (all required):\n\n* __condition__: This is the value used with `ocean-hydrate` to trigger the hydrator to be used.\n* __tagName__: Hydrators are implemented as custom elements. The `tagName` is the custom element tag name.\n* __renderMultiple__: This says that the custom element should be rendered for each element that uses the hydrator. Use `false` when hydrating is done without regard for the element. For example *idle* is `false` because it always just waits for CPU idle, so this only needs to be done once.\n* __script()__: A function which returns the custom element definition.\n\nThe following are optional properties:\n\n* __mutate(customElement, node)__: Gives you a change to modify the hydration custom element being rendered, for example to add information needed to perform hydration. __HydrateMedia__ uses this method to add the query to the custom element.\n\n## Relative links\n\nWhen performing hydration or adding the declarative shadow DOM polyfill, Ocean adds links that you provide it. You can provide full URLs or pathnames like `/js/dsd-polyfill.js`. If you'd like for these links to be relative, you can use the `relativeTo` function to create an `html` that will produce relative links. Here's how you might use it in a service worker context:\n\n```js\nimport 'https://cdn.spooky.click/ocean/1.3.1/shim.js?global';\nimport { Ocean } from 'https://cdn.spooky.click/ocean/1.3.1/mod.js';\n\nlet { relativeTo } = new Ocean({\n  document,\n  polyfillURL: '/js/dsd-polyfill.js'\n})\n\naddEventListener('fetch', event =\u003e {\n  let html = relativeTo(event.request.url);\n  let iter = html`\n    \u003c!doctype html\u003e\n    \u003chtml lang=\"en\"\u003e\n    \u003c!-- ... --\u003e\n  `;\n});\n```\n\nThe script tags added for the polyfill and for any element hydration will be relative to the event's URL.\n\n## Plugins\n\nOcean parses HTML into a DOM tree. Using plugins you can mutate the tree before it gets turned back into strings, allowing you to implement advanced behavior like syntax highlighting.\n\nFor the most part custom elements should be the way you customize HTML rendering; plugins are here for cases where you need to modify built-in elements.\n\nThe interface for a plugin is a function that returns an object with a `handle` method. The function is called during Ocean's internal optimization step:\n\n```js\nclass MyHighlighter {\n  handle(node, head) {\n    // Mutate this node, add anything to the head that you need.\n  }\n\n  static createInstance() {\n    return new MyHighlighter();\n  }\n}\n\nlet ocean = new Ocean({\n  document,\n  plugins: [MyHighlighter.createInstance]\n});\n```\n\n## Compatibility\n\nOcean is tested against popular web component libraries. These tests are not all inclusive, test contributions are very much welcome.\n\n| Library                                           | Compatible | Notes                                                                                                           |\n|---------------------------------------------------|------------|-----------------------------------------------------------------------------------------------------------------|\n| Vanilla                                           | ✔          |                                                                                                                 |\n| [Lit](https://lit.dev/)                           | ✔          |                                                                                                                 |\n| [Stencil](https://stenciljs.com/)                 | ✔          |                                                                                                                 |\n| [Haunted](https://github.com/matthewp/haunted)    | ✔          |                                                                                                                 |\n| [Atomico](https://atomicojs.github.io/)           | ✔          |                                                                                                                 |\n| [uce](https://github.com/WebReflection/uce)       | ✔          |                                                                                                                 |\n| [Preact](https://preactjs.com/)                   | ✔          |                                                                                                                 |\n| [petite-vue](https://github.com/vuejs/petite-vue) | ✔          |                                                                                                                 |\n| [Wafer](https://waferlib.netlify.app/)            | ✔          |                                                                                                                 |\n| [FAST](https://www.fast.design/)                  | ✖          | Heavily relies on DOM internals.                                                                                |\n| [Lightning Web Components](https://lwc.dev/)      | ✖          | I can't figure out how to export an LWC, if you can help see [#11](https://github.com/matthewp/ocean/issues/11) |","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmatthewp%2Focean","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmatthewp%2Focean","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmatthewp%2Focean/lists"}