@kraftwerkdesign/kd-components
v0.3.2
Published
Native web components for Kraftwerk agency projects.
Readme
@kraftwerkdesign/kd-components
Native web components for Kraftwerk agency projects. No runtime framework — every
component is a plain HTMLElement subclass that ships as ESM with TypeScript types.
Install
npm install @kraftwerkdesign/kd-componentsUsage
Components are not registered automatically on import. You choose how they get into the custom-element registry.
Register everything
import '@kraftwerkdesign/kd-components/auto'Register a single component
import '@kraftwerkdesign/kd-components/lite-vimeo'
import '@kraftwerkdesign/kd-components/lite-youtube'Register manually (tree-shakeable, no side effects)
import { defineLiteVimeo, defineLiteYouTube } from '@kraftwerkdesign/kd-components'
defineLiteVimeo() // registers <lite-vimeo>
defineLiteYouTube() // registers <lite-youtube>Each define* helper is idempotent (guarded by customElements.get) and accepts an
optional custom tag name:
defineLiteVimeo('kd-vimeo')The raw classes are exported too, if you need to subclass or register them yourself:
import { LiteVimeoEmbed, LiteYTEmbed } from '@kraftwerkdesign/kd-components'Cookie consent gating
Both components expose two optional static hooks so you can block the third-party iframe (and its cookies) until the visitor has granted consent. Wire them once from your app entry — if left unset, the components load normally.
import { LiteVimeoEmbed, LiteYTEmbed } from '@kraftwerkdesign/kd-components'
for (const El of [LiteVimeoEmbed, LiteYTEmbed]) {
// Checked on click. May open your consent UI. Return true to allow the load.
El.consentGate = () => requireConsent('functionality')
// Checked on autoload / scroll-into-view. Must NOT prompt — just report state.
El.consentReady = () => hasConsent('functionality')
}consentGate?: () => boolean— runs when the user activates the placeholder (click / Enter / Space). Returnfalseto suppress the iframe; this is the place to trigger your consent modal.consentReady?: () => boolean— runs for passive triggers (autoload, IntersectionObserver). Returnsfalseto defer loading without prompting; the element stays observed, so a later grant lets the next intersection load it.
Blocked-consent notice
When a click is blocked (consentGate returns false), the component shows a
dismissible notice pinned to the bottom of the poster explaining why the video did
not load. It carries a default message and a close button out of the box:
This video is blocked until you accept cookies.
The notice is a click-gate affordance only. Passive triggers that fail
consentReady (autoload, scroll-into-view) defer silently and never show it —
they leave the poster untouched so a later consent grant can load the video on the
next intersection.
Lifecycle. The notice is toggled by the consent-blocked class on the internal
#frame. It appears on a blocked click and hides again automatically when:
- the visitor dismisses it with the close button,
- consent is later granted and the iframe successfully loads, or
- the
videoid/playlistidchanges.
Dismissing it does not grant consent — the next click re-runs consentGate and
re-shows the notice if still blocked.
Override the message with the consent-notice slot (links and markup are allowed):
<lite-vimeo videoid="76979871">
<p slot="consent-notice">Accept cookies to watch. <a href="/privacy">Learn more</a></p>
</lite-vimeo>Theme it with custom properties set on the host element:
| Custom property | Default | Controls |
| ------------------------- | ------------------- | ------------------ |
| --consent-notice-bg | rgb(0 0 0 / 0.82) | Overlay background |
| --consent-notice-color | #fff | Text color |
| --consent-notice-radius | 6px | Corner radius |
For anything the custom properties don't cover, the notice and its close button are exposed as shadow parts:
lite-vimeo::part(consent-notice) {
font-size: 16px;
}
lite-vimeo::part(consent-notice-close) {
/* custom close-button styling */
}Accessibility. The notice container is a role="status" live region, the close
button carries aria-label="Dismiss" and a visible :focus-visible outline, and a
forced-colors fallback swaps to system Canvas/CanvasText colors with a border.
Components
Lightweight, fast-painting video embeds that only load the real iframe on
interaction (or when scrolled into view with autoload). Both components share
the same core API:
| Attribute | Description |
| --------------- | --------------------------------------------------------------------- |
| videoid | Video ID (required) |
| videotitle | Accessible title used for the play button, poster and iframe |
| videoplay | Localized "Play" label (default Play) |
| videostartat | Start time in seconds, e.g. 30 (Vimeo also accepts 1m30s) |
| params | Extra query params for the player URL, e.g. controls=0&rel=0 |
| posterloading | loading attr for the poster image (default lazy) |
| autoload | Load the iframe when scrolled into view via IntersectionObserver |
| autoplay | Autoplay (muted) when injected via autoload; clicks always autoplay |
Provide a custom poster with the poster slot — the component then skips its own
thumbnail requests entirely:
<lite-vimeo videoid="76979871">
<img slot="poster" src="/my-poster.jpg" alt="" />
</lite-vimeo>When the real iframe is injected, the element dispatches a bubbling custom event
(liteVimeoIframeLoaded / liteYoutubeIframeLoaded) with the video id in
event.detail.videoId.
If your Content-Security-Policy requires nonces on inline styles, set
window.liteVimeoNonce / window.liteYouTubeNonce before the elements are
registered.
Styling the play button
Both components share one play button (a dark disc, thin ring, and white triangle). Recolor and resize it with CSS custom properties set on the element:
| Custom property | Default | Controls |
| ----------------- | ------------------------ | ------------------- |
| --play-btn-fill | rgb(0 0 0 / 0.68) | Disc fill |
| --play-btn-ring | rgb(166 145 95 / 0.74) | Disc outline ring |
| --play-btn-icon | #fff | Play triangle |
| --play-btn-size | 130px | Button width/height |
lite-youtube {
--play-btn-fill: rgb(255 255 255 / 0.9);
--play-btn-ring: #f00;
--play-btn-icon: #f00;
--play-btn-size: 90px;
}For anything the custom properties don't cover, the button is exposed as a
shadow part — reach it via ::part(play-button) (including states like
::part(play-button):hover):
lite-vimeo::part(play-button) {
box-shadow: 0 0 0 4px #fff;
}<lite-vimeo>
<lite-vimeo videoid="76979871" videotitle="The Mountain"></lite-vimeo>| Attribute | Description |
| ----------- | ------------------------------ |
| videohash | Private-video hash (h param) |
The player URL always includes dnt=1 (Do Not Track).
<lite-youtube>
Adds playlist and YouTube Shorts support:
<lite-youtube videoid="dQw4w9WgXcQ" videotitle="Never Gonna Give You Up"></lite-youtube>| Attribute | Description |
| --------------- | --------------------------------------------------------------- |
| playlistid | Playlist ID (used instead of videoid) |
| posterquality | Thumbnail quality, e.g. hqdefault, maxresdefault |
| nocookie | Use youtube-nocookie.com (default; set ="false" to opt out) |
| short | Enable YouTube Shorts styling/behavior on mobile |
Development
npm install
npm run dev # serve demo/index.html with Vite
npm run build # build the publishable library into dist/
npm run format # prettierPublishing
Publishing is automated with npm OIDC trusted publishing — no NPM_TOKEN
secret, and provenance is attached automatically. Run one release script; it bumps
the version, tags it, pushes, and creates a GitHub Release, which triggers the
publish.yml workflow to typecheck, build, and publish to npm.
npm run release:patch # 0.1.0 -> 0.1.1
npm run release:minor # 0.1.0 -> 0.2.0
npm run release:major # 0.1.0 -> 1.0.0One-time setup: on npmjs.com, open the package settings and add a Trusted
Publisher pointing at the Kraftwerk-Design/kd-components repo and the
publish.yml workflow. Until the package exists on npm, the very first publish
may need to be run manually (npm publish) or the trusted publisher configured
ahead of time under the org.
License
MIT. The lite-youtube and lite-vimeo components are derived
from the MIT-licensed justinribeiro/lite-youtube
and slightlyoff/lite-vimeo.
