diff --git a/ui/packages/consul-ui/app/components/custom-element/README.mdx b/ui/packages/consul-ui/app/components/custom-element/README.mdx
new file mode 100644
index 000000000..4aa9d64c8
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/custom-element/README.mdx
@@ -0,0 +1,87 @@
+# CustomElement
+
+A renderless component to aid with the creation of HTML custom elements a.k.a
+WebComponents.
+
+All of the CustomElement component arguments are only used at construction
+time (within the components constructor) therefore they are, by design, static.
+You shouldn't be dynamically updating these values at all. They are only for
+type checking and documention purposes and therefore once defined/hardcoded
+they should only change if you as the author wishes to change them.
+
+The component is built from various other components, also see their documentaton
+for further details (``, ``).
+
+```hbs preview-template
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## Arguments
+
+All `descriptions` in attributes will be compiled out at build time as well as the `@description` attribute itself.
+
+| Attribute | Type | Default | Description |
+| :------------ | :------------- | :------ | :------------------------------------------------------------------------- |
+| element | string | | The custom tag to be used for the custom element. Must include a dash |
+| description | string | | Short 1 line description for the element. Think "git commit title" style |
+| attrs | attrInfo[] | | An array of attributes that can be used on the element |
+| slots | slotsInfo[] | | An array of slots that can be used for the element (100% compiled out) |
+| cssprops | cssPropsInfo[] | | An array of CSS properties that are relevant to the component |
+| cssparts | cssPartsInfo[] | | An array of CSS parts that can be used for the element (100% compiled out) |
+| args | argsInfo[] | | An array of Glimmer arguments used for the component (100% compiled out) |
+
+## Exports
+
+### custom
+
+| Attribute | Type | Description |
+| :--------- | :------- | :---------------------------------------------------------------------------------- |
+| connect | function | A did-insert-able callback for tagging an element to be used for the custom element |
+| disconnect | function | A will-destroy-able callback for destroying an element used for the custom element |
+
+### element
+
+| Attribute | Type | Description |
+| :--------- | :------- | :------------------------------------------------------------------------------- |
+| attributes | object | An object containing a reference to all the custom elements' observed properties |
+| * | | All other properties proxy through to the CustomElements class |
+
+
diff --git a/ui/packages/consul-ui/app/components/custom-element/index.hbs b/ui/packages/consul-ui/app/components/custom-element/index.hbs
new file mode 100644
index 000000000..a040d5eca
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/custom-element/index.hbs
@@ -0,0 +1,11 @@
+
+ {{yield
+ (hash
+ root=(fn this.setHost (fn shadow.host))
+ connect=(fn this.setHost (fn shadow.host))
+ Template=shadow.Template
+ disconnect=(fn this.disconnect)
+ )
+ this.element
+ }}
+
diff --git a/ui/packages/consul-ui/app/components/custom-element/index.js b/ui/packages/consul-ui/app/components/custom-element/index.js
new file mode 100644
index 000000000..6d52fdfcc
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/custom-element/index.js
@@ -0,0 +1,193 @@
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { tracked } from '@glimmer/tracking';
+import { assert } from '@ember/debug';
+
+const ATTRIBUTE_CHANGE = 'custom-element.attributeChange';
+const elements = new Map();
+const proxies = new WeakMap();
+
+const typeCast = (attributeInfo, value) => {
+ let type = attributeInfo.type;
+ const d = attributeInfo.default;
+ value = value == null ? attributeInfo.default : value;
+ if(type.indexOf('|') !== -1) {
+ assert(`"${value} is not of type '${type}'"`, type.split('|').map(item => item.replaceAll('"', '').trim()).includes(value));
+ type = 'string';
+ }
+ switch(type) {
+ case '':
+ case '':
+ case '':
+ case 'number': {
+ const num = parseFloat(value);
+ if(isNaN(num)) {
+ return typeof d === 'undefined' ? 0 : d;
+ } else {
+ return num;
+ }
+ }
+ case '':
+ return parseInt(value);
+ case '':
+ case 'string':
+ return (value || '').toString();
+ }
+}
+
+const attributeChangingElement = (name, Cls = HTMLElement, attributes = {}, cssprops = {}) => {
+ const attrs = Object.keys(attributes);
+
+ const customClass = class extends Cls {
+ static get observedAttributes() {
+ return attrs;
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ const prev = typeCast(attributes[name], oldValue);
+ const value = typeCast(attributes[name], newValue);
+
+ const cssProp = cssprops[`--${name}`];
+ if(typeof cssProp !== 'undefined' && cssProp.track === `[${name}]`) {
+ this.style.setProperty(
+ `--${name}`,
+ value
+ );
+ }
+
+ if(typeof super.attributeChangedCallback === 'function') {
+ super.attributeChangedCallback(name, prev, value);
+ }
+
+ this.dispatchEvent(
+ new CustomEvent(
+ ATTRIBUTE_CHANGE,
+ {
+ detail: {
+ name: name,
+ previousValue: prev,
+ value: value
+ }
+ }
+ )
+ );
+
+ }
+ }
+ customElements.define(name, customClass);
+ return () => {};
+}
+
+const infoFromArray = (arr, keys) => {
+ return (arr || []).reduce((prev, info) => {
+ let key;
+ const obj = {};
+ keys.forEach((item, i) => {
+ if(item === '_') {
+ key = i;
+ return;
+ }
+ obj[item] = info[i]
+ });
+ prev[info[key]] = obj;
+ return prev;
+ }, {});
+}
+const debounceRAF = (cb, prev) => {
+ if(typeof prev !== 'undefined') {
+ cancelAnimationFrame(prev);
+ }
+ return requestAnimationFrame(cb);
+}
+const createElementProxy = ($element, component) => {
+ return new Proxy($element, {
+ get: (target, prop, receiver) => {
+ switch(prop) {
+ case 'attrs':
+ return component.attributes;
+ default:
+ if(typeof target[prop] === 'function') {
+ // need to ensure we use a MultiWeakMap here
+ // if(this.methods.has(prop)) {
+ // return this.methods.get(prop);
+ // }
+ const method = target[prop].bind(target);
+ // this.methods.set(prop, method);
+ return method;
+ }
+
+ }
+ }
+ });
+}
+
+export default class CustomElementComponent extends Component {
+
+ @tracked $element;
+ @tracked _attributes = {};
+
+ __attributes;
+ _attchange;
+
+
+ constructor(owner, args) {
+ super(...arguments);
+ if(!elements.has(args.element)) {
+ const cb = attributeChangingElement(
+ args.element,
+ args.class,
+ infoFromArray(args.attrs, ['_', 'type', 'default', 'description']),
+ infoFromArray(args.cssprops, ['_', 'type', 'track', 'description'])
+ )
+ elements.set(args.element, cb);
+ }
+ }
+
+ get attributes() {
+ return this._attributes;
+ }
+
+ get element() {
+ if(this.$element) {
+ if(proxies.has(this.$element)) {
+ return proxies.get(this.$element);
+ }
+ const proxy = createElementProxy(this.$element, this);
+ proxies.set(this.$element, proxy);
+ return proxy;
+ }
+ return undefined;
+ }
+
+ @action
+ setHost(attachShadow, $element) {
+ attachShadow($element);
+ this.$element = $element;
+ this.$element.addEventListener(ATTRIBUTE_CHANGE, this.attributeChange);
+
+ (this.args.attrs || []).forEach(entry => {
+ const value = $element.getAttribute(entry[0]);
+ $element.attributeChangedCallback(entry[0], value, value)
+ });
+ }
+
+ @action
+ disconnect() {
+ this.$element.removeEventListener(ATTRIBUTE_CHANGE, this.attributeChange);
+ }
+
+ @action
+ attributeChange(e) {
+ e.stopImmediatePropagation();
+ // currently if one single attribute changes
+ // they all change
+ this.__attributes = {
+ ...this.__attributes,
+ [e.detail.name]: e.detail.value
+ };
+ this._attchange = debounceRAF(() => {
+ // tell glimmer we changed the attrs
+ this._attributes = this.__attributes;
+ }, this._attchange);
+ }
+}
diff --git a/ui/packages/consul-ui/app/components/shadow-host/README.mdx b/ui/packages/consul-ui/app/components/shadow-host/README.mdx
new file mode 100644
index 000000000..c83c58eb9
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/shadow-host/README.mdx
@@ -0,0 +1,29 @@
+# ShadowHost
+
+`ShadowHost` is a small renderless mainly utility component for easily attaching
+ShadowDOM to any applicable DOM node. It mainly exists to provide a context for
+passing around a reference to the element to be used for the shadow template,
+but named appropriately for recognition.
+
+If you are looking to write a custom element, please use the `CustomElement`
+component. If you are simply attaching ShadowDOM to a native HTML element then
+this is the component for you.
+
+```hbs preview-template
+
+
+
+```
+
+## Exports
+
+| Attribute | Type | Description |
+| :-------- | :---------------------- | :------------------------------------------------------------------------------- |
+| host | function | A did-insert-able callback for tagging an element to be used for the shadow root |
+| Template | ShadowTemplateComponent | ShadowTemplate component pre-configured with the shadow host |
diff --git a/ui/packages/consul-ui/app/components/shadow-host/index.hbs b/ui/packages/consul-ui/app/components/shadow-host/index.hbs
new file mode 100644
index 000000000..3c70ea8f0
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/shadow-host/index.hbs
@@ -0,0 +1,5 @@
+{{yield (hash
+ host=(fn this.attachShadow)
+ root=this.shadowRoot
+ Template=(component 'shadow-template' shadowRoot=this.shadowRoot)
+)}}
diff --git a/ui/packages/consul-ui/app/components/shadow-host/index.js b/ui/packages/consul-ui/app/components/shadow-host/index.js
new file mode 100644
index 000000000..c5eb1046e
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/shadow-host/index.js
@@ -0,0 +1,14 @@
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { tracked } from '@glimmer/tracking';
+
+export default class ShadowHostComponent extends Component {
+
+ @tracked shadowRoot;
+
+ @action
+ attachShadow($element) {
+ this.shadowRoot = $element.attachShadow({ mode: 'open' });
+ }
+
+}
diff --git a/ui/packages/consul-ui/app/components/shadow-template/README.mdx b/ui/packages/consul-ui/app/components/shadow-template/README.mdx
index 3f2556526..64d352a7d 100644
--- a/ui/packages/consul-ui/app/components/shadow-template/README.mdx
+++ b/ui/packages/consul-ui/app/components/shadow-template/README.mdx
@@ -9,7 +9,7 @@ Shadow DOM we have a `@shadowRoot` argument to which you would pass the actual
Shadow DOM element (which itself either open or closed). You can get a reference
to this by using the `{{attach-shadow}}` modifier.
-Additionally a `@stylesheets` argument is made available for you to optionally
+Additionally a `@styles` argument is made available for you to optionally
pass completely isolated, scoped, constructable stylesheets to be used for the
Shadow DOM tree (you can also continue to use `