Skip to main content
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:
pnpm g
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:
  1. componentWillLoad() — runs once before the first render; validate props here.
  2. componentDidLoad() — runs once after the first render; query DOM elements here.
  3. componentWillRender() — runs before every render.
  4. componentDidRender() — runs after every render.
  5. disconnectedCallback() — runs when the component is removed from the DOM.
Call checkPropValues() inside componentWillLoad() to validate initial prop values before the first render.

Build docs developers (and LLMs) love