alpine-rc
v0.1.4
Published
Alpine.js component plugin — global styles by default, scoped styles on demand, reactive props, slots
Maintainers
Readme
alpine-rc
Alpine.js plugin for reusable HTML components. Global styles work out of the box — no config needed. Scoped styles and Shadow DOM isolation are opt-in.
Installation
npm install alpine-rcimport Alpine from 'alpinejs'
import alpineRc from 'alpine-rc'
Alpine.plugin(alpineRc)
Alpine.start()CDN
Load before Alpine:
<script src="https://unpkg.com/alpine-rc/dist/alpine-rc.min.js" defer></script>
<script src="https://unpkg.com/alpinejs/dist/cdn.min.js" defer></script>Component files
A component is an .html file with a <template> wrapper:
<!-- components/card.html -->
<template>
<style scoped>
.card {
border: 1px solid #ddd;
padding: 16px;
border-radius: 8px;
}
.card h2 {
margin: 0;
}
</style>
<div class="card">
<h2 x-text="title"></h2>
<slot></slot>
</div>
</template>The <template> wrapper is stripped at load time. Styles are handled automatically — see Styles.
Basic usage
<div x-component.url="'./components/card.html'" :title="post.title">
<template x-slot>
<p x-text="post.body"></p>
</template>
</div>From an on-page template
<template id="card">
<div class="card">
<h2 x-text="title"></h2>
<slot></slot>
</div>
</template>
<div x-component="'card'" :title="post.title"></div>Props
Pass data to the component via :attr or x-bind:attr bindings on the host element. Props are reactive — when the bound value changes, the component updates without re-rendering.
<div
x-component.url="'./components/card.html'"
:title="post.title"
:count="post.likes"
:active="selectedId === post.id"
></div>Inside the component template, props are available directly:
<template>
<div :class="{ active }">
<h2 x-text="title"></h2>
<span x-text="count"></span>
</div>
</template>The component also inherits the full Alpine scope of the host element, so parent data like $store is accessible too.
Slots
Default slot
<div x-component.url="'./card.html'" :title="post.title">
<template x-slot>
<p>This goes into the default slot</p>
</template>
</div><!-- card.html -->
<template>
<div class="card">
<h2 x-text="title"></h2>
<slot></slot>
</div>
</template>Named slots
<div x-component.url="'./card.html'" :title="post.title">
<template x-slot>Main content</template>
<template x-slot="footer">
<button @click="save">Save</button>
</template>
</div><!-- card.html -->
<template>
<div class="card">
<h2 x-text="title"></h2>
<slot></slot>
<footer><slot name="footer"></slot></footer>
</div>
</template>Slots without matching content show their fallback children:
<slot>This renders if no slot content is provided</slot>Styles
Default — global styles work as-is
No configuration needed. Tailwind, CSS, SCSS, and any <link> stylesheets apply to the component naturally because it renders in the regular DOM.
<style> tags inside the component are extracted and injected into <head> once per component source.
<!-- x-component.url="'./card.html'" (no modifier) -->
<template>
<style>
/* Injected into <head> once — global scope */
.card {
padding: 16px;
}
</style>
<div class="card"></div>
</template>.scoped — isolated styles via data attribute
<style scoped> is transformed so selectors only match elements inside this component instance, using a data-arc-* attribute (similar to Vue SFC). Global styles and Tailwind still work.
<div x-component.url.scoped="'./card.html'"></div><!-- card.html -->
<template>
<style scoped>
/* Scoped: only applies inside this component */
.card {
padding: 16px;
}
</style>
<style>
/* Still global — injected to <head> as-is */
.badge {
border-radius: 99px;
}
</style>
<div class="card">...</div>
</template>The plugin generates a stable data-arc-[hash] attribute, adds it to every element in the template, and transforms the CSS selectors accordingly.
.isolated — Shadow DOM
Full style encapsulation. Global CSS and Tailwind do not apply inside the component.
<div x-component.url.isolated="'./card.html'"></div>To adopt global stylesheets into the shadow root:
<div x-component.url.isolated.with-styles="'./card.html'"></div>Modifiers reference
| Modifier | Description |
| ----------------------- | ---------------------------------------------- |
| .url | Load template from a same-origin URL |
| .url.external | Load template from a cross-origin URL |
| .scoped | Scope <style scoped> via data attribute |
| .isolated | Render in Shadow DOM |
| .isolated.with-styles | Shadow DOM + adopt global document stylesheets |
Lifecycle events
All events bubble and are dispatched on the host element.
el.addEventListener('rc:loading', ({ detail }) => console.log('loading', detail.source))
el.addEventListener('rc:loaded', ({ detail }) => console.log('loaded', detail.source))
el.addEventListener('rc:error', ({ detail }) => console.log('error', detail.error))| Event | Fires when | Detail |
| ------------ | ----------------------------------- | ------------------- |
| rc:loading | URL fetch starts (.url only) | { source } |
| rc:loaded | Component rendered | { source } |
| rc:error | Expression, fetch, or render failed | { source, error } |
Cross-origin URLs
Same-origin by default. Use .external to allow cross-origin:
<div x-component.url.external="'https://cdn.example.com/components/card.html'"></div>TypeScript
Types are included. No additional setup needed.
import type { RcLoadedEvent } from 'alpine-rc'
el.addEventListener('rc:loaded', (e: RcLoadedEvent) => {
console.log(e.detail.source)
})Production: static HTML baking
At build time you can pre-render all components into static HTML using vite-plugin-bake-alpine-components.
npm i -D vite-plugin-bake-alpine-components// vite.config.js
import bakeAlpineComponents from 'vite-plugin-bake-alpine-components'
export default {
plugins: [bakeAlpineComponents()],
}Use an explicit split between build-time and runtime directives:
s-*directives are baked at build time by the Vite plugin.x-*,:*, and@*directives are left untouched for Alpine runtime.
alpine-rc also normalizes s-* to their Alpine equivalents in dev/runtime mode so behavior stays consistent:
| Directive | Baked output | Dev/runtime equivalent |
| --- | --- | --- |
| s-text="expr" | static text | x-text |
| s-html="expr" | static innerHTML | x-html |
| s-class="expr" | merged into class | :class |
| s-style="expr" | merged into style | :style |
| s-show="expr" | style="display:none" if falsy | x-show |
| s-bind:attr="expr" | static attribute | :attr |
| <template s-for="x in list"> | expanded loop | <template x-for> |
| <template s-if="expr"> | included or removed | <template x-if> |
This keeps component authoring simple: use s-* for baked/static output and x-* for client reactivity.
Rules and constraints
s-if is only valid on <template> elements.
For conditional visibility on a regular element use s-show instead:
<!-- correct: conditional block -->
<template s-if="isAdmin">
<p>Admin panel</p>
</template>
<!-- correct: show/hide an element -->
<span s-show="label" s-text="label"></span>
<!-- wrong: s-if on a non-template element is not supported -->
<span s-if="label">...</span>Prop names must be lowercase.
HTML normalises attribute names to lowercase in the DOM. A binding like :showHint="..." becomes :showhint by the time alpine-rc reads it. Use all-lowercase names consistently in both the host binding and the component template:
<!-- host -->
<div x-component="'my-card'" :showhint="true"></div>
<!-- component template -->
<template s-if="showhint">...</template>x-data is always a runtime directive — never use s-bind:x-data.
Props are available inside x-data expressions through the scope chain, so write x-data directly and read props by name:
<!-- correct -->
<div x-data="{ count: Number(start) || 0 }">...</div>
<!-- wrong — s-bind:x-data breaks in dev mode -->
<div s-bind:x-data="{ count: Number(start) || 0 }">...</div>