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.
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+)
New in v2.0: Components are fully implemented with automatic discovery, ComponentBase class, and automatic context detection!
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:
Select the element in Webflow Designer
Add a custom attribute:
data-component=accordionThe component name must match the name in the
@componentdecorator
<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 fromdata-componentthis.context.id- Optional ID fromdata-component-idthis.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:
onPrepare() - Runs synchronously during
<head>loadUse for quick setup that doesn't require DOM manipulation
Context and element are available
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
One component class per file - Keep components organized in
/src/components/Use descriptive names - Component names should be clear:
accordion,multi-step-form,image-galleryScope your queries - Always query within
this.elementto avoid conflicts:// Good const items = this.element.querySelectorAll('.item'); // Bad - might affect other components const items = document.querySelectorAll('.item');Clean up resources - Remove event listeners if component is destroyed
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