@fionoble/preact-custom-elements
v1.0.0
Published
Framework for building Web Components with Preact
Downloads
9
Maintainers
Readme
Preact Custom Elements
A lightweight framework for building Web Components with Preact. Write your components in Preact and automatically convert them into standards-compliant Custom Elements.
Features
- Write components using familiar Preact syntax
- Automatic attribute-to-props mapping
- Support for camelCase props and kebab-case attributes
- Custom event dispatching
- Shadow DOM support (configurable)
- TypeScript support
- Minimal overhead
Installation
pnpm add preact-custom-elements preactQuick Start
1. Create a Preact Component
import { register } from 'preact-custom-elements';
import { useState } from 'preact/hooks';
interface CounterProps {
initialCount?: number;
step?: number;
onChange?: (count: number) => void;
}
function Counter({ initialCount = 0, step = 1, onChange }: CounterProps) {
const [count, setCount] = useState(initialCount);
const increment = () => {
const newCount = count + step;
setCount(newCount);
onChange?.(newCount);
};
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>Increment</button>
</div>
);
}
// Register as a custom element
register(Counter, 'my-counter', {
attributes: ['initial-count', 'step'],
shadow: true,
events: ['change']
});2. Use in HTML
<my-counter initial-count="10" step="5"></my-counter>
<script>
const counter = document.querySelector('my-counter');
// Listen to custom events
counter.addEventListener('change', (e) => {
console.log('Count changed:', e.detail);
});
// Update properties dynamically
counter.initialCount = 20;
</script>API
register(Component, tagName, options)
Registers a Preact component as a Custom Element.
Parameters
- Component (
ComponentType<P>): Your Preact component - tagName (
string): The custom element tag name (must contain a hyphen) - options (
WebComponentOptions): Configuration options
Options
interface WebComponentOptions {
// Attributes that will be observed and converted to props
attributes?: string[];
// Attributes to observe for changes (defaults to attributes)
observedAttributes?: string[];
// Enable Shadow DOM (true, false, or ShadowRootInit)
shadow?: boolean | ShadowRootInit;
// Event names to dispatch as CustomEvents
events?: string[];
}Features Explained
Attribute Mapping
Attributes are automatically converted from kebab-case to camelCase:
<my-component initial-value="10" user-name="John"></my-component>Maps to:
<MyComponent initialValue={10} userName="John" />Type Conversion
Attribute values are automatically parsed:
"true"/"false"→ boolean"123"→ number'{"key": "value"}'→ object (JSON.parse)- Other strings → string
Shadow DOM
Control Shadow DOM behavior:
// Enable with default settings
register(Component, 'my-element', { shadow: true });
// Disable Shadow DOM
register(Component, 'my-element', { shadow: false });
// Custom Shadow DOM config
register(Component, 'my-element', {
shadow: { mode: 'closed', delegatesFocus: true }
});Custom Events
Components can dispatch custom events:
function MyButton({ onClick }: { onClick?: (value: string) => void }) {
return (
<button onClick={() => onClick?.('clicked!')}>
Click me
</button>
);
}
register(MyButton, 'my-button', {
events: ['click']
});<my-button></my-button>
<script>
document.querySelector('my-button')
.addEventListener('click', (e) => {
console.log(e.detail); // 'clicked!'
});
</script>Slots
The framework supports both default and named slots when Shadow DOM is enabled. Slot content is automatically extracted from the host element and passed to your component via the slots prop.
Using Slots in Components
import { Slot } from 'preact-custom-elements';
interface CardProps {
slots?: {
default?: any[];
named: Record<string, any[]>;
};
}
function Card({ slots }: CardProps) {
const header = slots?.named?.header;
const footer = slots?.named?.footer;
return (
<div class="card">
{header && <div class="header">{header}</div>}
<div class="content">
{slots?.default || <Slot />}
</div>
{footer && <div class="footer">{footer}</div>}
</div>
);
}
register(Card, 'my-card', { shadow: true });Using Slots in HTML
<my-card>
<h2 slot="header">Card Title</h2>
<p>This goes in the default slot.</p>
<p>Multiple elements are supported!</p>
<div slot="footer">Footer content</div>
</my-card>Key Features:
- Automatic Extraction: Slot content is automatically extracted from the host element
- Named Slots: Use
slot="name"attribute to target named slots - Default Slot: Content without a slot attribute goes to the default slot
- Reactive Updates: Slot content automatically updates when children change
- Shadow DOM Only: Slots only work when Shadow DOM is enabled (
shadow: true)
The slots prop structure:
{
default?: VNode[]; // Default slot content
named: { // Named slot content
[slotName: string]: VNode[]
}
}Examples
Simple User Card
interface UserCardProps {
name?: string;
email?: string;
avatar?: string;
}
function UserCard({ name, email, avatar }: UserCardProps) {
return (
<div class="user-card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
<p>{email}</p>
</div>
);
}
register(UserCard, 'user-card', {
attributes: ['name', 'email', 'avatar'],
shadow: true
});<user-card
name="Jane Doe"
email="[email protected]"
avatar="https://example.com/avatar.jpg"
></user-card>Development
# Install dependencies
pnpm install
# Build the library
pnpm build
# Run example
pnpm exampleBuild Configuration
The project includes Rollup configuration for building both CommonJS and ES modules:
pnpm build # Creates dist/index.js and dist/index.esm.jsTypeScript
Full TypeScript support with type definitions included. The framework infers prop types from your component.
Browser Support
Works in all modern browsers that support:
- Custom Elements v1
- Shadow DOM v1
- ES2020
License
MIT
