Components

SSE's page routing model allows us to cleanly separate page-specific code in a very manageable way. But in some cases your code needs to be tied to a "component" rather than a page or path.

Component is a significant term in Webflow, but SSE "components" aren't strictly tied to Webflow Components. We'll be discussing both here- to distinguish...

  • "Components" (capitalized) will refer to Webflow Components.

  • "components" (lowercase) will refer to SSE components.

These are some examples of "components" that might use code:

  • A specially configured SwiperJS setup

  • A custom component you've built such as an accordion

  • Specialized form validation

  • A fancy multi-step form

  • A cool color-picker

  • Also, any Webflow Component that needs code attached to its functionality

Often, these "components" need to be reused on multiple pages, and your design team might for example duplicate a quiz or a contact form to another page at any time- ideally the dev team would not need to do anything for that component to automatically work on that new page.

Goals

  • Efficient code execution, only run code when it's needed

  • Code isolation, e.g. the code & CSS for a multi-step form should be distinct from the rest of your source code

  • Reusability. Your development work should be easy to repurpose on other projects you build.

  • Webflow Component support. Take full advantage of the Webflow Team's work on components and leverage it in every way possible to maximize the finished "smart" component.

  • Create a design paradigm that supports the possibility of multiple "component" instances per page.

Implementation (v2.0+)

Creating a Component

Components extend ComponentBase for automatic element and context detection:

import { ComponentBase, component } from '@sygnal/sse-core';

@component('accordion')
export class AccordionComponent extends ComponentBase {

  protected onPrepare(): void {
    // Synchronous setup
    // this.element and this.context automatically available
    console.log('Accordion component:', this.context.name);
  }

  protected async onLoad(): Promise<void> {
    // Asynchronous execution
    const items = this.element.querySelectorAll('.accordion-item');

    items.forEach(item => {
      item.addEventListener('click', () => {
        item.classList.toggle('open');
      });
    });
  }
}

Using Components in Webflow

To use a component, add the data-component attribute to any element in Webflow:

  1. Select the element in Webflow Designer

  2. Add a custom attribute: data-component = accordion

  3. The component name must match the name in the @component decorator

<div data-component="accordion" class="accordion-wrapper">
  <!-- Your accordion HTML -->
</div>

Multiple Component Instances

Components automatically support multiple instances on the same page. Each instance gets its own separate class instance:

<!-- First accordion -->
<div data-component="accordion" data-component-id="main-faq">
  <!-- FAQ content -->
</div>

<!-- Second accordion -->
<div data-component="accordion" data-component-id="secondary-info">
  <!-- Info content -->
</div>

Both will initialize independently with their own AccordionComponent instance.

Automatic Context Detection

When extending ComponentBase, you automatically get:

this.element

The HTMLElement the component is bound to:

protected async onLoad(): Promise<void> {
  // Direct access to the component's root element
  const children = this.element.querySelectorAll('.child-item');
  this.element.addEventListener('click', () => {
    console.log('Component clicked');
  });
}

this.context

Component metadata automatically extracted:

protected onPrepare(): void {
  console.log(this.context.name);           // 'accordion'
  console.log(this.context.id);             // 'main-faq'
  console.log(this.context.dataAttributes); // All data-* attributes
}

Available context properties:

  • this.context.name - Component name from data-component

  • this.context.id - Optional ID from data-component-id

  • this.context.dataAttributes - All data-* attributes as key-value pairs

Accessing Page Information from Components

New in v2.0: Components can access the current page via the singleton pattern:

import { ComponentBase, component, PageBase } from '@sygnal/sse-core';

@component('navigation')
export class NavigationComponent extends ComponentBase {

  protected async onLoad(): Promise<void> {
    // Access current page info
    const page = PageBase.getCurrentPage();

    if (page) {
      const info = page.getPageInfo();
      console.log('Current page ID:', info.pageId);
      console.log('Collection ID:', info.collectionId);
      console.log('Item slug:', info.itemSlug);

      // Adjust navigation based on current page
      if (info.collectionId === 'blog') {
        this.element.classList.add('blog-nav');
      }
    }
  }
}

Use the public getPageInfo() accessor (the pageInfo property is protected) whenever a component reads Webflow page context.

Component Discovery and Registration

New in v2.0: Components are automatically discovered using the @component decorator!

Automatic Discovery

Simply decorate your component class and import it:

// src/components/accordion.ts
import { ComponentBase, component } from '@sygnal/sse-core';

@component('accordion')
export class AccordionComponent extends ComponentBase {
  // Implementation
}

Register in routes.ts

Import your component files to trigger decorator registration:

// src/routes.ts
import { RouteDispatcher, getAllPages } from "@sygnal/sse-core";
import { Site } from "./site";

// Import pages
import "./pages/home";
import "./pages/blog";

// Import components to register them
import "./components/accordion";
import "./components/navigation";
import "./components/form-validator";

export const routeDispatcher = (): RouteDispatcher => {
    const dispatcher = new RouteDispatcher(Site);
    dispatcher.routes = getAllPages();
    return dispatcher;
}

That's it! SSE automatically finds all data-component attributes in your HTML and initializes the matching components.

Component Lifecycle

Components follow the same lifecycle as pages:

  1. onPrepare() - Runs synchronously during <head> load

    • Use for quick setup that doesn't require DOM manipulation

    • Context and element are available

  2. onLoad() - Runs asynchronously after DOM is ready

    • Use for event listeners, DOM queries, async operations

    • Full DOM access guaranteed

@component('my-component')
export class MyComponent extends ComponentBase {

  protected onPrepare(): void {
    // Quick synchronous setup
    console.log('Component preparing:', this.context.name);
  }

  protected async onLoad(): Promise<void> {
    // Async operations, DOM manipulation
    await this.loadData();
    this.attachEventListeners();
  }

  private async loadData() {
    // Fetch data, etc.
  }

  private attachEventListeners() {
    this.element.addEventListener('click', () => {
      // Handle click
    });
  }
}

Data Attributes for Configuration

Use data attributes to configure component behavior:

<div
  data-component="slider"
  data-component-id="hero-slider"
  data-autoplay="true"
  data-speed="3000"
  data-loop="true"
>
  <!-- Slider content -->
</div>

Access in your component:

@component('slider')
export class SliderComponent extends ComponentBase {

  protected async onLoad(): Promise<void> {
    // Access configuration from data attributes
    const config = this.context.dataAttributes;

    const autoplay = config['autoplay'] === 'true';
    const speed = parseInt(config['speed'] || '2000');
    const loop = config['loop'] === 'true';

    this.initializeSlider({ autoplay, speed, loop });
  }

  private initializeSlider(config: any) {
    // Initialize with config
  }
}

Best Practices

  1. One component class per file - Keep components organized in /src/components/

  2. Use descriptive names - Component names should be clear: accordion, multi-step-form, image-gallery

  3. Scope your queries - Always query within this.element to avoid conflicts:

    // Good
    const items = this.element.querySelectorAll('.item');
    
    // Bad - might affect other components
    const items = document.querySelectorAll('.item');
  4. Clean up resources - Remove event listeners if component is destroyed

  5. Use data attributes for configuration - Keep components flexible and reusable

Example: Complete Component

Here's a complete example of a tabs component:

import { ComponentBase, component } from '@sygnal/sse-core';

@component('tabs')
export class TabsComponent extends ComponentBase {

  protected async onLoad(): Promise<void> {
    const tabButtons = this.element.querySelectorAll('[data-tab-button]');
    const tabPanes = this.element.querySelectorAll('[data-tab-pane]');

    tabButtons.forEach((button, index) => {
      button.addEventListener('click', () => {
        // Remove active from all
        tabButtons.forEach(btn => btn.classList.remove('active'));
        tabPanes.forEach(pane => pane.classList.remove('active'));

        // Add active to clicked
        button.classList.add('active');
        tabPanes[index]?.classList.add('active');
      });
    });

    // Activate first tab by default
    tabButtons[0]?.classList.add('active');
    tabPanes[0]?.classList.add('active');
  }
}

Use in Webflow:

<div data-component="tabs" class="tabs-wrapper">
  <div class="tab-buttons">
    <button data-tab-button>Tab 1</button>
    <button data-tab-button>Tab 2</button>
    <button data-tab-button>Tab 3</button>
  </div>
  <div class="tab-content">
    <div data-tab-pane>Content 1</div>
    <div data-tab-pane>Content 2</div>
    <div data-tab-pane>Content 3</div>
  </div>
</div>

Last updated