Implement the theme
Install exact artifacts, preserve native semantics and bring j3w1 components into Vue, React, Astro or plain HTML.
Consuming j3w1 components
@j3w1/ui serves web applications that support Custom Elements. Native
children stay real HTML, so labels, form ownership, validation, fieldsets and
reset keep browser semantics. The design contract stays in tokens/ and
spec/. An empty <j3w1-text-field> invents no label or input: start from
the maintained markup on the component’s page.
Install and pin
Keep the exact package version and your lockfile in source control.
npm install --save-exact @j3w1/ui@4.0.0
That needs npm publication, an owner release action; do not assume it happened. Until then, download the release tarball, check it against its published digest and install the file:
npm install --save-exact ./j3w1-ui-4.0.0.tgz
npm run ui:build and npm run ui:pack build the same artifact here. Never
pin a branch URL.
Load one component
import '@j3w1/ui/tokens.css';
import '@j3w1/ui/styles/text-field.css';
import '@j3w1/ui/register/text-field';
Load the token CSS once, then copy the component’s maintained markup. A
registration entry loads the nested registrations it needs, and a stylesheet
carries its full style closure. @j3w1/ui/register and @j3w1/ui/styles.css
load the whole catalogue, for a gallery; ordinary apps import per component.
- Class imports such as
@j3w1/ui/components/text-fieldhave no registration side effects and are safe during SSR; register in the browser. - Registering the same module twice is safe; a foreign class already registered under the same name is rejected. Use one package version per document.
- Compact is the default density (D-038); set
data-density="comfortable"on the document or a containing element to opt in to the larger mode. - No font binaries are bundled; the stack falls back to locally installed monospace fonts.
- Component CSS is scoped to its tag: load it after broad resets and avoid later selectors that restyle every button or input. Customise only through the semantic token variables, layout and documented classes. Scope any deliberate host override and record it in your integration; a new literal colour is a deviation, not a theme variant. Keep focus, selection, contrast and forced-colours behaviour.
Themed choices, dates and native controls
j3w1-select renders its open list in theme colours (D-025) as a select-only
combobox or a multiselectable listbox; its native select stays the source of
FormData, validation, defaults, disabled fieldsets and reset, and the
no-JavaScript fallback. Use j3w1-combobox when users type to filter. To
enhance existing application markup without moving native controls or
changing their names:
import '@j3w1/ui/tokens.css';
import '@j3w1/ui/styles/controls.css';
import { enhanceControls } from '@j3w1/ui/enhance/choice';
const controls = enhanceControls(document.querySelector('#application'));
// On application unmount:
controls.destroy();
- It watches controls inserted into or removed from that root only.
Frameworks keep owning their native selects,
value bindings and change handlers; in Vue, call it after mount and destroy
it in
onBeforeUnmount. Completeselectcopy bundles include the same renderer; do not hand-roll another dropdown. - Date and time inputs get a themed editor (ISO entry, a calendar, step buttons) bound to the native constraints, defaults and reset; the original inputs stay the no-JavaScript fallback. Where an engine exposes them as text, the enhancement validates ISO values, range and step itself. Keys are on each component page.
- Checkbox, radio, range and file-button chrome is themed; number spinners are removed in favour of the Number field’s step controls. Browser-owned file and system dialogs stay host UI.
- Test the visible combobox or listbox: Playwright
selectOptionon the hidden native select does not exercise the rendered menu.
Vue 3 JavaScript SFCs
Declare j3w1-* tags as Custom Elements in the Vite config, import
registration in the browser entry or the SFC, and put the maintained native
markup in the template:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue({ template: { compilerOptions: {
isCustomElement: tag => tag.startsWith('j3w1-'),
} } })],
});
Submit with @submit.prevent and new FormData(event.target); listen with
@j3w1-change="onChange" and read event.detail; pass objects and arrays as
properties (:data.prop="series"); bind values with the native input’s
v-model, not a component modelValue. A ref exposes the element’s methods
after mount. apps/demo is an independent package consumer.
React, Astro and plain HTML
React 19. Use native JSX children and defaultValue for an uncontrolled
input, a ref for complex properties and methods, and
addEventListener('j3w1-change', handler) removed in the effect cleanup. A
native form’s onSubmit receives the real form.
Astro. Import CSS in frontmatter and registration in a browser <script>,
never in server frontmatter.
Plain HTML. Use the copy bundle’s index.html, or serve the package’s
dist directory over HTTP and load its relative ESM registration module with
<script type="module">; file: URLs are not supported. A complete copy
bundle has no bare runtime imports and does not depend on this website.
Attributes, properties and events
- Boolean attributes work by presence:
disabled="false"still disables. Remove the attribute or set.disabled = false. - Arrays, objects and
Fileinstances go in properties; the generated API table lists them. - Setting
.valueor.checkedchanges state without faking a user event; nativeform.reset()restores the defaults. - Native input and change events behave as usual. Documented
j3w1-*events are bubbling, composed, cancelableCustomEvents with a structureddetail; cancelling one stops a default action only where its API says so, and never undoes a value change it reports.
Forms and validation
- Keep each native control’s
name, label and form relationships.FormDataholds successful native controls only. Validation rules, uploads, persistence and authorisation are your app’s; usecheckValidity()orreportValidity().
Composition, lifecycle and styling
- Light DOM: native children are the composition surface. Give every instance
unique IDs and update each
for,aria-*and fragment reference with them. Callrefresh()after your framework replaces the children of a connected instance, and never let a framework and a widget both own the same children. Disconnecting removes listeners, timers and open overlays; reconnecting reattaches them.
Copy a complete bundle
npx --package=@j3w1/ui@4.0.0 j3w1-ui copy dialog --out ./vendor/j3w1/dialog
npx --package=@j3w1/ui@4.0.0 j3w1-ui kit --components text-field,button,dialog --out ./vendor/j3w1/task
From a tarball install, run the installed j3w1-ui binary instead of npx.
The CLI checks every source byte before writing and refuses unknown
components, symlinks and existing destinations. Keep element.html,
component.css, tokens.css, the whole runtime/ closure, the manifest and
the licence notices together; a kit gives each bundle its own directory.
index.json is the discovery index, and contracts/<id>.json and
examples/<id>.json keep small tasks bounded.
Troubleshooting and upgrades
- Install into a clean fixture; read the notes below, the changelog and the
changed component contracts (
contracts/<id>.json, the component pages). - Exercise your real form and keyboard flows.
- Update the lockfile and your integration record together.
- 4.0.0 (D-033): prose and interface text are rose; content headings use
color.text.heading; bold inherits its colour. Replace heading uses oftext.prosewithtext.headingand drop colour rules attached only to bold (docs/rose-typography-migration.md). - 3.0.0 (D-032): terminal slot 6 is coral
#ff7a66, the prompt gets its own background role and bright rose#ffa2a7text, andcolor.primitive.ansi-readable.6is removed. - 2.0.0 (D-029, D-030): the dim terminal slots and two code syntax reds are
lifted; opt-in
code.hued.*syntax roles are added. - 1.0.0 (D-023): True Black / Rose becomes the default; read
docs/true-black-rose-migration.mdbefore replacing 0.1.0 tokens.
Component behaviour did not change in 2.0.0 or 3.0.0. If something breaks: an unknown tag needs browser registration (and the Vue compiler setting); an unstyled control needs tokens and its stylesheet; a missing form value means checking the native control’s name, state and owning form, never adding a hidden duplicate; a registration conflict means removing the second version; a copy integrity failure means fetching the pinned artifact again, never bypassing the check. Browser results cover only the tested artifact and environment; manual screen-reader and device testing are separate evidence.
Maintained framework examples
These exact source files are compiled and exercised against the packed artifact in isolated HTML, Vue, React and Astro fixtures. They demonstrate native submission, validation, reset and disabled fieldsets. Use the component pages for the native markup and API of other controls.
html/index.html
<!doctype html><html lang="en" data-density="compact"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Independent consumer</title></head><body><main><h1>Independent consumer</h1><form id="consumer-form"><fieldset id="fields"><j3w1-text-field id="field"><div class="text-field"><label class="text-field-label" for="project">Project</label><input class="text-field-input" id="project" name="project" value="Initial" required></div></j3w1-text-field><j3w1-checkbox id="check"><label class="checkbox checkbox-option"><span class="checkbox-control"><input class="checkbox-input" type="checkbox" name="enabled" value="yes"><svg class="checkbox-check" aria-hidden="true" viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3.5 8.5l3 3 6-6"></path></svg></span><span class="checkbox-text">Enabled</span></label></j3w1-checkbox><j3w1-button><button class="button" type="submit">Save</button></j3w1-button><j3w1-button><button class="button button-secondary" type="reset">Reset</button></j3w1-button></fieldset></form><p role="status" id="result">Ready</p></main><script type="module" src="./main.js"></script></body></html>html/main.js
import '@j3w1/ui/register/button';
import '@j3w1/ui/register/text-field';
import '@j3w1/ui/register/checkbox';
import '@j3w1/ui/tokens.css';
import '@j3w1/ui/styles/button.css';
import '@j3w1/ui/styles/text-field.css';
import '@j3w1/ui/styles/checkbox.css';
document.querySelector('form').addEventListener('submit',event=>{event.preventDefault();document.querySelector('#result').textContent=JSON.stringify(Object.fromEntries(new FormData(event.currentTarget)));});vue/App.vue
<script setup>
import {ref} from 'vue';
const result=ref('Ready');
const save=event=>{result.value=JSON.stringify(Object.fromEntries(new FormData(event.target)));};
</script>
<template><main><h1>Independent consumer</h1><form id="consumer-form" @submit.prevent="save"><fieldset id="fields"><j3w1-text-field id="field"><div class="text-field"><label class="text-field-label" for="project">Project</label><input class="text-field-input" id="project" name="project" value="Initial" required></div></j3w1-text-field><j3w1-checkbox id="check"><label class="checkbox checkbox-option"><span class="checkbox-control"><input class="checkbox-input" type="checkbox" name="enabled" value="yes"><svg class="checkbox-check" aria-hidden="true" viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3.5 8.5l3 3 6-6"></path></svg></span><span class="checkbox-text">Enabled</span></label></j3w1-checkbox><j3w1-button><button class="button" type="submit">Save</button></j3w1-button><j3w1-button><button class="button button-secondary" type="reset">Reset</button></j3w1-button></fieldset></form><p role="status" id="result">{{ result }}</p></main></template>
vue/main.js
import '@j3w1/ui/register/button';
import '@j3w1/ui/register/text-field';
import '@j3w1/ui/register/checkbox';
import '@j3w1/ui/tokens.css';
import '@j3w1/ui/styles/button.css';
import '@j3w1/ui/styles/text-field.css';
import '@j3w1/ui/styles/checkbox.css';
import {createApp} from 'vue';import App from './App.vue';createApp(App).mount('#app');vite.config.js
import {defineConfig} from 'vite';import vue from '@vitejs/plugin-vue';import path from 'node:path';export default defineConfig({base:'./',plugins:[vue({template:{compilerOptions:{isCustomElement:tag=>tag.startsWith('j3w1-')}}})],build:{outDir:'built/web',rollupOptions:{input:Object.fromEntries(['html','vue','react','gallery'].map(name=>[name,path.resolve(name,'index.html')]))}}});
react/main.js
import '@j3w1/ui/register/button';
import '@j3w1/ui/register/text-field';
import '@j3w1/ui/register/checkbox';
import '@j3w1/ui/tokens.css';
import '@j3w1/ui/styles/button.css';
import '@j3w1/ui/styles/text-field.css';
import '@j3w1/ui/styles/checkbox.css';
import React from 'react';import {createRoot} from 'react-dom/client';
function App(){const[result,setResult]=React.useState('Ready');return React.createElement('main',null,React.createElement('h1',null,'Independent consumer'),React.createElement('form',{id:'consumer-form',onSubmit:event=>{event.preventDefault();setResult(JSON.stringify(Object.fromEntries(new FormData(event.currentTarget))));}},React.createElement('fieldset',{id:'fields'},React.createElement('j3w1-text-field',{id:'field'},React.createElement('div',{className:'text-field'},React.createElement('label',{className:'text-field-label',htmlFor:'project'},'Project'),React.createElement('input',{className:'text-field-input',id:'project',name:'project',defaultValue:'Initial',required:true}))),React.createElement('j3w1-checkbox',{id:'check'},React.createElement('label',{className:'checkbox checkbox-option'},React.createElement('span',{className:'checkbox-control'},React.createElement('input',{className:'checkbox-input',type:'checkbox',name:'enabled',value:'yes'}),React.createElement('svg',{className:'checkbox-check','aria-hidden':'true',viewBox:'0 0 16 16',width:16,height:16,fill:'none',stroke:'currentColor',strokeWidth:1.5},React.createElement('path',{d:'M3.5 8.5l3 3 6-6'}))),React.createElement('span',{className:'checkbox-text'},'Enabled'))),React.createElement('j3w1-button',null,React.createElement('button',{className:'button',type:'submit'},'Save')),React.createElement('j3w1-button',null,React.createElement('button',{className:'button button-secondary',type:'reset'},'Reset')))),React.createElement('p',{role:'status',id:'result'},result));}
createRoot(document.querySelector('#app')).render(React.createElement(App));
astro/src/pages/index.astro
---
import '@j3w1/ui/tokens.css';
import '@j3w1/ui/styles/button.css';
import '@j3w1/ui/styles/text-field.css';
import '@j3w1/ui/styles/checkbox.css';
---
<!doctype html><html lang="en" data-density="compact"><head><meta charset="utf-8"/><title>Independent consumer</title><meta name="viewport" content="width=device-width,initial-scale=1"/></head><body><main><h1>Independent consumer</h1><form id="consumer-form"><fieldset id="fields"><j3w1-text-field id="field"><div class="text-field"><label class="text-field-label" for="project">Project</label><input class="text-field-input" id="project" name="project" value="Initial" required></div></j3w1-text-field><j3w1-checkbox id="check"><label class="checkbox checkbox-option"><span class="checkbox-control"><input class="checkbox-input" type="checkbox" name="enabled" value="yes"><svg class="checkbox-check" aria-hidden="true" viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3.5 8.5l3 3 6-6"></path></svg></span><span class="checkbox-text">Enabled</span></label></j3w1-checkbox><j3w1-button><button class="button" type="submit">Save</button></j3w1-button><j3w1-button><button class="button button-secondary" type="reset">Reset</button></j3w1-button></fieldset></form><p role="status" id="result">Ready</p></main><script>import '@j3w1/ui/register/button';import '@j3w1/ui/register/text-field';import '@j3w1/ui/register/checkbox';document.querySelector('form').addEventListener('submit',event=>{event.preventDefault();document.querySelector('#result').textContent=JSON.stringify(Object.fromEntries(new FormData(event.currentTarget)));});</script></body></html>astro.config.mjs
import {defineConfig} from 'astro/config';export default defineConfig({srcDir:'./astro/src',outDir:'./built/astro',base:'/astro/',output:'static'});