BEEQ components are built with StencilJS, a compiler that generates standard Web Components. Every component lives in packages/beeq/src/components/ and follows a consistent file and code structure.
Generating a component
Use the built-in generator to scaffold a new component. It creates all the necessary files and wires things up automatically:
Follow the interactive CLI prompts to name your component and configure its initial options.
Component file structure
Each component is contained in its own directory named after the component (e.g., bq-button/):
packages/beeq/src/components/bq-button/
├── __tests__/
│ └── bq-button.e2e.ts # End-to-end tests (Puppeteer)
├── _storybook/
│ ├── bq-button.mdx # Storybook docs page
│ └── bq-button.stories.tsx # Storybook stories
├── scss/
│ └── bq-button.scss # Component styles
├── bq-button.tsx # Component implementation
├── bq-button.types.ts # Type definitions and allowed values
└── readme.md # Auto-generated API docs
Naming conventions
- Element tag: always prefixed with
bq-, e.g., bq-button, bq-dialog, bq-select.
- Class name: PascalCase without prefix, e.g.,
BqButton.
- Events: camelCase prefixed with
bq, e.g., bqClick, bqFocus, bqBlur.
- CSS custom properties:
--bq-{component}--{property}, e.g., --bq-button--border-radius.
- Types file: named
bq-{component}.types.ts, exports const arrays and type aliases derived from them.
StencilJS component anatomy
Here is the structure of bq-button.tsx as a reference:
@Component decorator
Defines the HTML tag, stylesheet, and shadow DOM configuration:
@Component({
tag: 'bq-button',
styleUrl: './scss/bq-button.scss',
formAssociated: true,
shadow: {
delegatesFocus: true,
},
})
export class BqButton {
// ...
}
@Element
Provides a reference to the host element:
@Element() el!: HTMLBqButtonElement;
@State — internal reactive state
Used for values that affect rendering but are not part of the public API. Decorated properties are listed in alphabetical order:
@State() private hasPrefix = false;
@State() private hasSuffix = false;
@Prop — public properties
Public API of the component. Use reflect: true when the attribute should mirror the property on the element. Include a JSDoc comment for each prop:
/** The appearance style to apply to the button */
@Prop({ reflect: true }) appearance?: TButtonAppearance = 'primary';
/** If `true`, the button will be disabled (no interaction allowed) */
@Prop() disabled?: boolean = false;
/** The size of the button */
@Prop({ reflect: true }) size?: TButtonSize = 'medium';
@Watch — prop watchers
Watch decorators react to prop changes. Stacked @Watch decorators on a single method are a common pattern for shared validation:
@Watch('appearance')
@Watch('type')
@Watch('size')
@Watch('variant')
checkPropValues() {
validatePropValue(BUTTON_APPEARANCE, 'primary', this.el, 'appearance');
validatePropValue(BUTTON_TYPE, 'button', this.el, 'type');
validatePropValue(BUTTON_SIZE, 'medium', this.el, 'size');
validatePropValue(BUTTON_VARIANT, 'standard', this.el, 'variant');
}
@Event — custom events
All events are prefixed with bq and documented with JSDoc:
/** Handler to be called when the button loses focus. */
@Event() bqBlur: EventEmitter<HTMLBqButtonElement>;
/** Handler to be called when the button gets focus. */
@Event() bqFocus: EventEmitter<HTMLBqButtonElement>;
/** Handler to be called when the button is clicked. */
@Event() bqClick: EventEmitter<HTMLBqButtonElement>;
@Method — public methods
Public methods are exposed on the host element and must always be async:
/** Programmatically focuses the button. */
@Method()
async focusButton(): Promise<void> {
this.el.focus();
}
render()
The render() method is always the last method in the class. Use the <Host> wrapper to set top-level element attributes and inline CSS custom properties:
render() {
const isLink = isDefined(this.href);
const TagElem = isLink ? 'a' : 'button';
const style = {
...(this.border && { '--bq-button--border-radius': `var(--bq-radius--${this.border})` }),
};
return (
<Host style={style}>
<TagElem
class={{
'bq-button': true,
[`bq-button--${this.appearance}`]: true,
disabled: this.disabled,
loading: this.loading,
}}
disabled={this.disabled}
part="button"
>
<span class="bq-button__prefix" part="prefix">
<slot name="prefix" onSlotchange={this.handleSlotChange} />
</span>
<span class="bq-button__label" part="label">
<slot />
</span>
<span class="bq-button__suffix" part="suffix">
<slot name="suffix" onSlotchange={this.handleSlotChange} />
</span>
</TagElem>
</Host>
);
}
Types file
Each component has a .types.ts file that exports allowed values as const arrays alongside derived TypeScript types:
// bq-button.types.ts
export const BUTTON_SIZE = ['small', 'medium', 'large'] as const;
export type TButtonSize = (typeof BUTTON_SIZE)[number];
export const BUTTON_APPEARANCE = ['primary', 'secondary', 'link', 'text'] as const;
export type TButtonAppearance = (typeof BUTTON_APPEARANCE)[number];
export const BUTTON_VARIANT = ['standard', 'ghost', 'danger'] as const;
export type TButtonVariant = (typeof BUTTON_VARIANT)[number];
This pattern makes validation straightforward — the same constant array is used at runtime (for prop validation) and at compile time (via the derived type).
CSS custom properties
Expose styling hooks via CSS custom properties following the naming convention --bq-{component}--{property}:
// bq-button.scss
:host {
--bq-button--border-radius: var(--bq-radius--m);
--bq-button--border-color: transparent;
--bq-button--border-style: solid;
--bq-button--border-width: var(--bq-border-width--m);
}
Document all custom properties in the JSDoc block at the top of the .tsx file using @cssprop:
/**
* @cssprop --bq-button--border-color - Button border color
* @cssprop --bq-button--border-radius - Button border radius
*/
Component lifecycle order
Stencil calls lifecycle hooks in the following order:
componentWillLoad() — runs once before the first render; validate props here.
componentDidLoad() — runs once after the first render; query DOM elements here.
componentWillRender() — runs before every render.
componentDidRender() — runs after every render.
disconnectedCallback() — runs when the component is removed from the DOM.
Call checkPropValues() inside componentWillLoad() to validate initial prop values before the first render.