@stand8/kai
v0.1.0
Published
Kai, the Stand8 ELEV8 assistant orb, as a framework-free <kai-orb> custom element with an optional React wrapper.
Maintainers
Readme
@stand8/kai
Kai, the Stand8 ELEV8 assistant orb, as a framework-free <kai-orb> custom element. It is the exact Kai from ELEV8: the same morphing gradient body, the same blinking eyes, and eyes that follow the pointer. Each state renders pixel for pixel like the app (checked by a visual test against ELEV8's own renders).
- One self-registering script, under 6 KB gzipped, with no dependencies.
- Shadow DOM, so page styles cannot break it and it cannot leak styles into the page.
- An optional React wrapper for React and Next.js.
Plain HTML and Webflow
Add the script once, then use the element anywhere:
<script src="https://cdn.jsdelivr.net/npm/@stand8/[email protected]/dist/kai-orb.js" defer></script>
<kai-orb></kai-orb>
<kai-orb size="24" mood="thinking" live></kai-orb>
<kai-orb size="96" follow label="Kai, the Stand8 assistant"></kai-orb>In Webflow, put the <script> in Site settings → Custom code → Footer code (or a page's footer code), and the <kai-orb> tags in an Embed element. Pin an exact version (@0.1.0) so the site never changes under you; @0.1 takes compatible updates automatically.
Change the orb from a script by setting properties:
const kai = document.querySelector('kai-orb');
kai.mood = 'listening';
kai.live = true;npm (bundlers, Vite, plain ES modules)
npm install @stand8/kaiimport '@stand8/kai'; // registers <kai-orb>
const kai = document.createElement('kai-orb');
kai.size = 24;
kai.mood = 'thinking';
kai.live = true;
document.body.append(kai);To register under another tag name, call defineKaiOrb('my-kai'). It is safe to call more than once, and it does nothing on the server.
React and Next.js
import { KaiOrb } from '@stand8/kai/react';
export function AskKaiHeader({ isStreaming, hasUnseenAnswer }: { isStreaming: boolean; hasUnseenAnswer: boolean }) {
return (
<KaiOrb
size={24}
mood={isStreaming ? 'thinking' : 'idle'}
live={isStreaming}
ready={hasUnseenAnswer && !isStreaming}
/>
);
}@stand8/kai/react is a client module ('use client'), so it works from Next.js Server Components without extra wrapping. It server-renders as a plain <kai-orb> tag and the element takes over in the browser. The ref is the KaiOrbElement. React 18 and 19 are supported; React is an optional peer dependency, needed only for this entry point.
Importing @stand8/kai/react also types <kai-orb> for JSX, if you would rather write the element directly.
Attributes
Every attribute is also a property (follow-target is followTarget). Setting a property updates the attribute, and the reverse.
| Attribute | Values | Default | What it does |
| --- | --- | --- | --- |
| size | number, in CSS px | 56 | Diameter. 16, 24, and 56 keep ELEV8's tuned eye geometry; any other size is the 56 px face scaled, shadow included. |
| mood | idle, listening, thinking, happy | idle | What the face is doing (see below). The body keeps morphing in every mood. |
| live | boolean | off | An answer is streaming: faster morph, a gentle breath, and a ring pulsing out of the orb. |
| follow | boolean | off | The eyes follow the pointer anywhere on the page while idle. On touch screens they look toward the last tap. |
| follow-target | CSS selector | none | With follow, while the matching element (or anything inside it) has focus, the eyes look at its caret. Use it for a chat input. |
| ready | boolean | off | The cobalt answer-ready dot in the top-right corner, ringed in --kai-bg. |
| theme | light, dark, auto | light | Which brand palette to use. auto follows the visitor's prefers-color-scheme. |
| blink | blink="false" turns it off | on | The idle loop: a blink every 4.8 s and a glance every 9 s. |
| awake | boolean | off | Wide-awake eyes (1.2× taller) while idle, as when ELEV8's Ask Kai button is hovered. Hovering the orb itself does the same on devices with a mouse. |
| label | text | none | The accessible name. Without it the orb is decorative. |
Boolean attributes are on when present (<kai-orb live>) and off when absent. blink is the exception because it defaults to on.
Moods
| Mood | Looks like | When ELEV8 uses it |
| --- | --- | --- |
| idle | Eyes open, blinking and glancing; follows the pointer with follow | Resting |
| listening | Eyes drop and open a little wider | Someone is typing in the composer |
| thinking | Eyes narrowed, looking up from side to side | An answer is streaming (always with live) |
| happy | One 1.1 s squint, then back to open eyes | An answer just landed |
happy plays its squint each time the mood changes to happy. ELEV8 switches back to idle afterwards and shows ready until the answer is seen.
Colors and styling
The brand colors are CSS custom properties, so a host can retint Kai without touching the package:
kai-orb {
--kai-daybreak: #abb1ff; /* gradient start, top-left */
--kai-cloud: #3d9cfa; /* gradient middle, live ring */
--kai-cobalt: #5215fc; /* gradient end, shadow, eye outline, ready dot */
--kai-bg: #fafafa; /* the page background, used to ring the ready dot */
}| Property | Light | Dark |
| --- | --- | --- |
| --kai-daybreak | #ABB1FF | #C4C8FF |
| --kai-cloud | #3D9CFA | #5BB0FF |
| --kai-cobalt | #5215FC | #7B4DFF |
| --kai-bg | #FAFAFA | #0B0C0F |
A value you set wins in every theme. To retint per theme, scope it yourself, for example .dark kai-orb { --kai-cobalt: … }. Set --kai-bg to your page background if you use ready on a background other than these.
The internal parts can be styled with ::part(): kai-orb::part(orb), ::part(eye), ::part(ring), and ::part(dot).
kai-orb is display: inline-flex. To reserve its space before the script loads, add kai-orb:not(:defined) { display: inline-block; width: 56px; height: 56px; } (with your size).
Accessibility
By default Kai is decorative: the element gets aria-hidden="true", which is right when it sits next to a visible "Ask Kai" label. When the orb is the only thing that says what a control is, give it a name with label; it then gets role="img" and that aria-label:
<button type="button"><kai-orb size="24" label="Ask Kai"></kai-orb></button>Kai never takes focus and never handles clicks; put it inside your own button or link.
Reduced motion
When the visitor has asked for less motion (prefers-reduced-motion: reduce), every animation stops and the eyes stop following the pointer. The eyes still sit where each mood puts them (higher while thinking, lower and wider while listening), just without movement. This matches ELEV8, and it updates live if the visitor changes the setting.
Following the pointer
All orbs with follow share one passive pointermove listener on window, throttled to one update per animation frame. It is added when the first following orb connects and removed when the last one disconnects or drops follow. The eyes reach full deflection when the pointer is about 240 px away, and never leave the face.
follow-target is resolved inside the same document or shadow root as the orb. For an <input> or <textarea>, the caret position is measured from the text before it; for other elements (such as contenteditable), from the current selection.
Browser support
Current Chrome, Edge, Safari, and Firefox: anything with CSS color-mix() (Chrome and Edge 111, Safari 16.2, Firefox 113). The eased glance uses CSS.registerProperty where available; elsewhere the glance simply steps.
Development
pnpm install
pnpm demo # builds, then serves the demo at http://127.0.0.1:4173/demo/
pnpm test # unit tests (Vitest + happy-dom)
pnpm build # dist/: ESM + CJS + types, and the self-registering dist/kai-orb.js
pnpm size # fails if dist/kai-orb.js is over 6 KB gzipped
pnpm test:visual # visual parity with ELEV8 (needs pnpm build and Playwright's Chromium)
pnpm check # typecheck, lint, unit tests, build, sizeThe visual parity test renders every state (idle, blink, awake, listening, thinking with its ring, happy, answer ready; light and dark) at 1024 px and compares it with ELEV8's own renders in test/visual/baselines/. Those baselines were produced by rendering the real ELEV8 component (AskOrb.tsx with the app's compiled globals.css, recruiting-dash 4c119cc5) in Playwright 1.63's Chromium, with the animations frozen at the same poses. The test fails if more than 0.1% of the pixels differ. It runs in CI inside the matching Playwright container.
The look is a copy of ELEV8's .ask-* rules (in src/styles.ts). If ELEV8 changes Kai, update that file, re-render the baselines with the ELEV8 reference script, and the visual test confirms the two match again.
Releasing
npm version patch(orminor/major) bumpspackage.json, commits, and tagsvX.Y.Z.git push --follow-tags.
The Release workflow checks the tag against package.json, runs pnpm check, and publishes to npm with the NPM_TOKEN repository secret. It adds npm provenance only when this repository is public, because npm rejects provenance from private repositories. jsDelivr serves a new version within a few minutes of publishing.
License
Proprietary. Copyright Stand8, all rights reserved. The package is publicly downloadable so Stand8 sites can load it, but Kai is a Stand8 brand asset; see LICENSE.
