UUI Coding Style
February 25, 2026 · View on GitHub
Component file structure
Each component folder uses a registration file pattern (inspired by Shoelace):
button/
├── button.ts # Registration file (side effect: registers the element)
├── button.element.ts # Pure class definition (no side effects)
├── button.test.ts # Tests
├── button.story.ts # Storybook story
└── README.md
Why this pattern?
-
No side effects in class files —
.element.tsfiles export pure classes. They can be imported for type checking, subclassing, or testing without triggeringcustomElements.define(). -
Simpler build — Rollup's
preserveModulesdrops pure re-export barrel files (index.ts) from the output. Registration files have a side effect (defineElement()call), so Rollup naturally preserves them. This eliminates the need for a glob hack to force-emit barrel entry points. -
Better discoverability —
button.jsis more descriptive thanindex.jsin stack traces, import completions, and browser dev tools.
Filename conventions
- Filenames drop the
uui-prefix:button.element.ts, notuui-button.element.ts - Class names keep the prefix:
UUIButtonElement - Tag names keep the prefix:
uui-button - Event files keep their PascalCase names:
UUICardEvent.ts
Registration file anatomy
Structure: imports → side effects → types → exports.
// button/button.ts
import { defineElement } from '../../internal/registration/index.js';
import { UUIButtonElement } from './button.element.js';
defineElement('uui-button', UUIButtonElement);
declare global {
interface HTMLElementTagNameMap {
'uui-button': UUIButtonElement;
}
}
export * from './button.element.js';
export { UUIButtonElement as default } from './button.element.js';
For multi-element folders (e.g. table/ with 6 elements), one registration file imports and registers all sibling elements. The primary element (matching the folder name) is the default export.
defineElement function
defineElement supports two calling styles:
- Direct call (used in registration files):
defineElement('uui-button', UUIButtonElement) - Decorator (used only in standalone example elements):
@defineElement('uui-button')
Both are defensive — they check for a valid element name and skip registration if the element is already defined.
Import conventions
- Within a component folder: import from the
.elementfile for the class, or from the registration file for side effects - Cross-component imports: import from the registration file (
../button/button.js) to ensure the element is registered - Test files: must import the registration file at the top (e.g.
import './button.js';) - Type-only imports: use
import typefor imports used only in type positions (enforced by@typescript-eslint/consistent-type-imports)