create-new-component
GitHub用于根据项目规范创建新的 Lit Web 组件,包括目录结构、组件类、样式、测试和 Storybook 故事,确保符合编码指南。
Trigger Scenarios
Install
npx skills add IgniteUI/igniteui-webcomponents --skill create-new-component -g -y
SKILL.md
Frontmatter
{
"name": "create-new-component",
"description": "Create a new Lit web component following project conventions, including component class, styles, tests, Storybook story, and proper exports"
}
Create New Component
Creates a new Lit web component that follows the project conventions. Read the Coding Guidelines for the rules behind the steps below.
When to Use
- "Create a new progress-bar component"
- "Add a new stepper component to the library"
Related Skills
- add-component-property - Add properties after creating the component
- update-component-styles - Modify component styles
Required Context
Confirm with the user before starting:
- Name:
progress-bar→ tagigc-progress-bar, classIgcProgressBarComponent - Purpose: one-line description used verbatim in the public API docs
- Public API: initial properties, events, slots, CSS parts
- Kind: plain display component, container, or form-associated control
Steps
1. Create the directory structure
mkdir -p src/components/[name]/themes/{light,dark,shared}
2. Create the component class
src/components/[name]/[name].ts:
import { html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import { registerComponent } from '#internals/definitions/register.js';
import { addThemingController } from '#theming/theming-controller.js';
import { styles } from './themes/[name].base.css.js';
import { styles as shared } from './themes/shared/[name].common.css.js';
import { all } from './themes/themes.js';
/**
* [One-line description of what the component is for.]
*
* @element igc-[name]
*
* @slot - [Default slot description]
*
* @csspart base - [Description of the CSS part]
*/
export default class Igc[Name]Component extends LitElement {
public static readonly tagName = 'igc-[name]';
public static override styles = [styles, shared];
/* blazorSuppress */
public static register(): void {
registerComponent(Igc[Name]Component);
}
//#region Public attributes and properties
/**
* [Property description]
* @attr some-prop
* @default 'default-value'
*/
@property({ reflect: true })
public someProp = 'default-value';
//#endregion
constructor() {
super();
addThemingController(this, all);
}
protected override render() {
return html`
<div part="base">
<slot></slot>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'igc-[name]': Igc[Name]Component;
}
}
Key points:
- Cross-cutting imports go through
#internals/*,#theming/*and#animations/*; component imports stay relative. All specifiers end in.js. registerComponent(Self, ...dependencies)— pass every component rendered in the template.- Region fences and the member order follow the component structure.
- Internal API is
_-prefixed; no native private fields (#). - Only primitives may be attributes; complex types get
attribute: false. - For ARIA use
addInternalsController; for keyboard useaddKeybindings; for slot state useaddSlotController. See the controllers table.
3. Create the SCSS files
SCSS resolves against the src and node_modules load paths — use package-style specifiers,
never relative ones. Indentation in SCSS is 4 spaces.
themes/[name].base.scss — structure and layout, theme-agnostic:
@use 'styles/common/component';
@use 'styles/utilities' as *;
:host {
display: block;
}
[part~='base'] {
// Structural styles
}
themes/light/_themes.scss — digest the schemas from igniteui-theming:
@use 'styles/utilities' as *;
@use 'igniteui-theming/sass/themes/schemas/components/light/[name]' as *;
$base: digest-schema($light-[name]);
$material: digest-schema($material-[name]);
$bootstrap: digest-schema($bootstrap-[name]);
$fluent: digest-schema($fluent-[name]);
$indigo: digest-schema($indigo-[name]);
themes/dark/_themes.scss mirrors it with the dark schemas (no $base).
Then, per theme:
themes/light/[name].shared.scss— emits the full variable set from$basethemes/light/[name].{bootstrap,material,fluent,indigo}.scss—diff($base, $theme)themes/dark/[name].{bootstrap,material,fluent,indigo}.scss—diff(light.$base, $theme)themes/shared/[name].common.scss— cross-theme styling that reads the variablesthemes/shared/[name].{bootstrap,material,fluent,indigo}.scss— per-theme structural tweaks (optional)
// themes/light/[name].bootstrap.scss
@use 'styles/utilities' as *;
@use 'themes' as *;
$theme: $bootstrap;
:host {
@include css-vars-from-theme(diff($base, $theme));
}
[!NOTE] A brand-new component only has schemas once they are added to
igniteui-theming. Until then, declare the CSS variables directly inthemes/shared/[name].common.scssand keep the light/dark files empty rather than inventing values per theme.
4. Create the theme aggregator
themes/themes.ts is the only hand-written TypeScript file in the directory:
import { css } from 'lit';
import type { Themes } from '#theming/types.js';
// Dark Overrides
import { styles as bootstrapDark } from './dark/[name].bootstrap.css.js';
import { styles as fluentDark } from './dark/[name].fluent.css.js';
import { styles as indigoDark } from './dark/[name].indigo.css.js';
import { styles as materialDark } from './dark/[name].material.css.js';
// Light Overrides
import { styles as bootstrapLight } from './light/[name].bootstrap.css.js';
import { styles as fluentLight } from './light/[name].fluent.css.js';
import { styles as indigoLight } from './light/[name].indigo.css.js';
import { styles as materialLight } from './light/[name].material.css.js';
import { styles as shared } from './light/[name].shared.css.js';
const light = {
shared: css`
${shared}
`,
bootstrap: css`
${bootstrapLight}
`,
material: css`
${materialLight}
`,
fluent: css`
${fluentLight}
`,
indigo: css`
${indigoLight}
`,
};
const dark = {
shared: css`
${shared}
`,
bootstrap: css`
${bootstrapDark}
`,
material: css`
${materialDark}
`,
fluent: css`
${fluentDark}
`,
indigo: css`
${indigoDark}
`,
};
export const all: Themes = { light, dark };
5. Transpile the styles
npm run build:styles
This generates a .css.ts next to each .scss (imported as .css.js). The generated files
are gitignored — never edit or commit them. Only files matching
*.{base,common,shared,material,bootstrap,indigo,fluent}.scss are picked up; anything else is
silently skipped.
6. Write the tests
src/components/[name]/[name].spec.ts:
import { elementUpdated, expect, fixture, html } from '@open-wc/testing';
import { defineComponents } from '#internals/definitions/defineComponents.js';
import Igc[Name]Component from './[name].js';
describe('[Name]', () => {
before(() => {
defineComponents(Igc[Name]Component);
});
it('passes the a11y audit', async () => {
const el = await fixture<Igc[Name]Component>(html`<igc-[name]></igc-[name]>`);
await expect(el).shadowDom.to.be.accessible();
await expect(el).to.be.accessible();
});
it('is initialized with the proper default values', async () => {
const el = await fixture<Igc[Name]Component>(html`<igc-[name]></igc-[name]>`);
expect(el.someProp).to.equal('default-value');
});
it('updates on property change', async () => {
const el = await fixture<Igc[Name]Component>(html`<igc-[name]></igc-[name]>`);
el.someProp = 'new-value';
await elementUpdated(el);
expect(el.someProp).to.equal('new-value');
});
});
Drive user interaction through the shared simulators (simulateClick, simulateKeyboard, …)
from #internals/testing/simulate.spec.js, and use
createFormAssociatedTestBed from #internals/testing/form-testbed.spec.js for form-associated
controls.
7. Create the Storybook story
stories/[name].stories.ts — the filename must match the tag name, and the generated block
must be fenced by // region default / // endregion:
import type { Meta, StoryObj } from '@storybook/web-components-vite';
import { html } from 'lit';
import { defineComponents, Igc[Name]Component } from 'igniteui-webcomponents';
defineComponents(Igc[Name]Component);
// region default
const metadata: Meta<Igc[Name]Component> = {
title: '[Name]',
component: 'igc-[name]',
};
export default metadata;
type Story = StoryObj<Igc[Name]Component>;
// endregion
export const Basic: Story = {
render: (args) => html`
<igc-[name] .someProp=${args.someProp}>Content</igc-[name]>
`,
};
Everything inside the region is regenerated in the next step — write only the stories.
8. Export and generate metadata
Add the export to src/index.ts in alphabetical order:
export { default as Igc[Name]Component } from './components/[name]/[name].js';
Then regenerate the derived artifacts:
npm run cem # custom-elements.json from the JSDoc
npm run build:meta # the `// region default` block of the story
9. Verify
npm run check # aliases, dependency rules, types
npm run lint # oxlint, lit-analyzer, oxfmt, stylelint
npm run test
Finally, add a CHANGELOG entry.
Documentation Conventions
Every JSDoc description on a public class, property, method, event, slot, CSS part or CSS
custom property is consumed verbatim by custom-elements.json, the generated story
metadata, the published API docs and the Angular / React / Blazor wrappers. Write product
documentation, not internal notes.
Never put igc- tag names in prose. Refer to components by their plain-English name — "the
carousel", "the tile manager", "toggle buttons".
// ❌ Wrong — tag names leak into the docs of every framework wrapper
/**
* The `igc-carousel` presents a set of `igc-carousel-slide`s.
*
* @slot - Renders `igc-toggle-button` component.
* @csspart svg - The igc-circular-progress SVG element.
*/
// ✅ Right
/**
* The carousel presents a set of slides.
*
* @slot - Renders the toggle buttons of the group.
* @csspart svg - The circular progress SVG element.
*/
Tag names are allowed only in the @element tag, fenced @example blocks, literal
event/attribute names that contain igc- (e.g. the "igc-change-theme" window event), and
@internal/@hidden members or non-exported internals.
Describe the thing, not the attribute. @attr already says it is an attribute.
| ❌ Avoid | ✅ Prefer |
|---|---|
The label attribute of the control. |
The label of the control. |
The outlined attribute of the control. |
Whether the control has an outlined appearance. |
Gets/Sets the name for all child radios. |
The name applied to all radio buttons in the group. |
an empty value will return an empty string |
an empty value returns an empty string |
- Booleans start with "Whether …" and describe the
truestate accurately — check the implementation, don't trust the property name (hideIndicatorsis "Whether the carousel should skip rendering of the indicator controls"). - Use present tense; avoid "will".
- Keep the description as the leading summary paragraph; don't append it to
@element. - Public methods that return something get an
@returnstag.
Validation Checklist
- Component at
src/components/[name]/[name].ts, single default export -
tagName,stylesandregister()static members defined - Cross-cutting imports use
#internals/#theming/#animations - Theming controller added in the constructor
- JSDoc with
@element,@slot,@csspart,@cssproperty,@eventas applicable - No
igc-tag names in description prose -
HTMLElementTagNameMapdeclaration present - SCSS scaffold complete (base, shared, light, dark) and
themes.tsaggregator wired - Spec file with the mandatory a11y audit
- Story file named after the tag, with a
// region defaultfence - Exported from
src/index.tsalphabetically -
npm run cem && npm run build:metarun, generated story region committed -
npm run check,npm run lintandnpm run testpass - CHANGELOG updated
Common Pitfalls
| Symptom | Cause / Fix |
|---|---|
Cannot resolve ./themes/*.css.js |
npm run build:styles not run, or the .scss filename doesn't match the glob |
| Styles never apply in a theme | Missing entry in themes.ts, or addThemingController not called |
| Component ignores theme switching | No addThemingController(this, all) in the constructor |
[part='base'] stops matching |
partMap emits multiple names — use [part~='base'] |
| TypeScript doesn't know the tag | Missing declare global { interface HTMLElementTagNameMap { … } } |
| Story descriptions are stale | The // region default block was hand-edited — fix the JSDoc and regenerate |
| Story never updates | Filename doesn't match the tag name, or the region fence is missing |
npm run check fails on imports |
A relative import into internals/theming/animations, or a missing alias in scripts/_package.json |
Reference Examples
| Kind | Component | Shows |
|---|---|---|
| Simple display | src/components/badge/badge.ts |
Theming, slot controller, partMap, internals ARIA |
| Form-associated | src/components/input/input.ts |
Form mixin, validators, input-shell template, ARIA target |
| Composite / overlay | src/components/select/select.ts |
ARIA projection, keybindings, popover |
| Container | src/components/card/card.ts |
Registering sub-components, composition |
Version History
- 7.2.4 Current 2026-08-20 11:42


