The official build toolkit for creating RapidWeaver Elements
Build powerful, reusable web components for RapidWeaver without the complexity. This toolkit handles the heavy lifting so you can focus on what matters: creating great elements.
rw-elements-tools is a development toolkit that simplifies the process of creating custom elements for RapidWeaver, the popular Mac website builder. It provides:
- A powerful CLI for building and watching your element files
- Ready-to-use controls for common UI patterns (colors, spacing, typography, and more)
- Shared utilities for generating Tailwind CSS classes
- Smart optimization that automatically removes unused code from your builds
- Theme developers who want to create custom RapidWeaver elements
- Agencies building bespoke elements for client projects
- Developers looking to extend RapidWeaver's capabilities
- Anyone who wants to contribute elements to the RapidWeaver ecosystem
| Without rw-elements-tools | With rw-elements-tools |
|---|---|
| Manually write complex JSON config files | Use intuitive JavaScript configuration |
| Copy/paste utility code between elements | Import from a shared library |
| Bloated output with unused code | Automatic dead code elimination |
| Manual rebuilds on every change | Watch mode for instant updates |
npm install --save-dev rw-elements-toolsCreate a new directory for your element pack project and initialize it:
mkdir my-element-pack
cd my-element-pack
npm init -y
npm install --save-dev rw-elements-toolsYour project needs a packs folder containing your element pack(s). Each pack follows this structure:
my-element-pack/
├── package.json
├── packs/ # Default packs directory
│ └── MyPack.elementsdevpack/ # Your pack (must end in .elementsdevpack)
│ └── components/
│ └── com.yourcompany.elementname/ # Component folder (must start with com.)
│ ├── properties.config.json # Source config (you edit this)
│ ├── properties.json # Generated output (don't edit)
│ ├── hooks.source.js # Source hooks (you edit this)
│ └── hooks.js # Generated output (don't edit)
└── node_modules/
Key naming conventions:
- Pack folders must end with
.elementsdevpack - Component folders must start with
com.(e.g.,com.mycompany.button) - Source files:
properties.config.jsonandhooks.source.js - Generated files:
properties.jsonandhooks.js
Create the folder structure for your first element:
mkdir -p packs/MyPack.elementsdevpack/components/com.mycompany.buttonCreate a minimal properties.config.json:
{
"groups": [
{
"title": "Content",
"icon": "text.alignleft",
"properties": [
{
"title": "Button Text",
"id": "buttonText",
"text": {
"default": "Click Me"
}
}
]
}
]
}Create a hooks.source.js that uses the shared hook utilities:
function transformHook(rw) {
// Props are accessed via rw.props - property IDs from properties.config.json
const { buttonText } = rw.props;
// Build CSS classes using the shared classnames utility
const classes = classnames()
.add(globalSpacing(rw))
.add(globalBgColor(rw))
.toString();
return {
classes,
buttonText
};
}
exports.transformHook = transformHook;Note: The
buttonTextprop corresponds to the"id": "buttonText"defined inproperties.config.json. All property IDs become available onrw.props. Functions likeclassnames(),globalSpacing(), andglobalBgColor()come from the shared hooks library—no imports needed.
Add these scripts to your package.json:
{
"scripts": {
"build": "rw-build all",
"build:properties": "rw-build properties",
"build:hooks": "rw-build hooks",
"dev": "rw-build all --watch"
}
}# One-time build
npm run build
# Or watch for changes during development
npm run devThat's it! The build tool will generate properties.json and hooks.js files in each component folder.
# Build all properties and hooks
npx rw-build all
# Build properties only
npx rw-build properties
# Build hooks only
npx rw-build hooks
# Watch for changes
npx rw-build all --watch # Watch both properties and hooks
npx rw-build properties --watch # Watch properties only
npx rw-build hooks --watch # Watch hooks onlyThe packs directory can be configured via multiple methods (in priority order):
rw-build all --packs ./my-elementsRW_PACKS_DIR=./my-elements npm run build{
"rw-elements-tools": {
"packsDir": "./my-elements"
}
}// rw-elements-tools.config.js
export default {
packsDir: './my-elements'
}If no configuration is provided, looks for ./packs in the project root.
- Overview
- Directory Structure
- Properties Build System
- Shared Hooks Build System
- Controls
- Properties
- Configuration File Format
- Adding New Controls
- Adding New Properties
- Adding Shared Hooks
- Advanced Features
- Build Commands
- Programmatic Usage
The build system processes properties.config.json files located in element component directories and generates expanded properties.json files. This allows developers to:
- Reuse common UI controls across multiple elements via
globalControlreferences - Share property definitions (like spacing values, font weights) via
usereferences - Override defaults per-element while maintaining a single source of truth
- Apply theme defaults for consistent theming across elements
properties.config.json Controls (controls/)
│ │
▼ ▼
┌─────────────────────────────────────┐
│ build-properties.js │
│ • Expands globalControl references │
│ • Resolves 'use' property refs │
│ • Applies overrides & defaults │
│ • Injects Advanced group controls │
└─────────────────────────────────────┘
│
▼
properties.json
(consumed by RapidWeaver)
rw-elements-tools/
├── bin/
│ └── cli.js # CLI entry point (rw-build command)
├── build-properties.js # Properties build script
├── build-shared-hooks.js # Shared hooks build script
├── config.js # Configuration resolver
├── index.js # Package entry point
├── package.json # npm package config
├── README.md # This documentation
├── controls/ # Reusable UI control definitions
│ ├── index.js # Exports all controls
│ ├── alignment/ # Flexbox/Grid alignment controls
│ ├── Animations/ # Animation and scroll animation controls
│ ├── Background/ # Background color, image, gradient, video
│ ├── Borders/ # Border and outline controls
│ ├── core/ # Essential controls (ID, CSSClasses, etc.)
│ ├── Effects/ # Box shadow, opacity, filters, blur
│ ├── grid-flex/ # Grid and flexbox item controls
│ ├── interactive/ # Button, input, link, filter controls
│ ├── Layout/ # Position, overflow, visibility, z-index
│ ├── Overlay/ # Overlay color, gradient, image
│ ├── Sizing/ # Width, height, min/max sizing
│ ├── Spacing/ # Margin and padding controls
│ ├── Transforms/ # Rotate, scale, skew, translate
│ ├── Transitions/ # Transition timing and properties
│ └── typography/ # Text color, decoration, styles
├── properties/ # Reusable property value definitions
│ ├── index.js # Exports all properties
│ ├── Slider.js # Slider value ranges
│ ├── FontWeight.js # Font weight options
│ └── ... # Other property definitions
└── shared-hooks/ # Shared JavaScript hook functions
├── animations/ # Animation and reveal functions
├── background/ # Background processing functions
├── borders/ # Border and outline functions
├── core/ # Essential utilities (classnames, etc.)
├── effects/ # Visual effects (opacity, filters)
├── interactive/ # Link and filter functions
├── layout/ # Layout and positioning
├── navigation/ # Navigation component styles
├── sizing/ # Dimensions and aspect ratios
├── spacing/ # Margin and padding functions
├── transforms/ # CSS transform functions
├── transitions/ # CSS and Alpine transitions
└── typography/ # Text and font style functions
The properties build script is executed via:
cd src
npm run buildThis runs node build.js which:
-
Finds all config files matching the glob pattern:
../**/*.elementsdevpack/components/com.**/**/properties.config.json -
Processes each config file through the following pipeline:
- Parse the JSON configuration
- Set up the Advanced group with injected controls (CSSClasses, ID)
- Process each property group
- Expand
globalControlreferences - Resolve
useproperty references - Apply overrides and theme defaults
- Write the output to
properties.json
For each property in a config file:
┌─────────────────────────────────────────────────────────────┐
│ 1. Check for globalControl │
│ └─ If present: Load control from Controls registry │
│ └─ If absent: Pass through with 'use' resolution only │
├─────────────────────────────────────────────────────────────┤
│ 2. Deep clone the control (avoid mutations) │
├─────────────────────────────────────────────────────────────┤
│ 3. Apply property overrides │
│ └─ String values: Replace directly │
│ └─ Object values: Shallow merge │
├─────────────────────────────────────────────────────────────┤
│ 4. Apply default values │
│ └─ Primitive defaults: Set control.default │
│ └─ Object defaults: Merge into theme properties │
├─────────────────────────────────────────────────────────────┤
│ 5. Apply theme defaults │
│ └─ themeColor, themeFont, themeBorderRadius, etc. │
├─────────────────────────────────────────────────────────────┤
│ 6. Transform IDs using {{value}} template │
│ └─ "prefix{{value}}Suffix" → "prefixControlIdSuffix" │
├─────────────────────────────────────────────────────────────┤
│ 7. Process nested globalControls recursively │
├─────────────────────────────────────────────────────────────┤
│ 8. Resolve 'use' references to Properties │
└─────────────────────────────────────────────────────────────┘
The shared hooks build system combines reusable JavaScript utility functions with component-specific hook code, then applies dead code elimination to produce optimized output.
shared-hooks/**/*.js Component hooks.source.js
│ │
▼ ▼
┌─────────────────────────────────────────┐
│ build-shared-hooks.js │
│ • Concatenates shared + component code │
│ • Applies esbuild dead code elimination │
│ • Keeps only code reachable from │
│ transformHook function │
└─────────────────────────────────────────┘
│
▼
hooks.js (optimized)
(consumed by RapidWeaver)
- Find all source files: Scans
packs/forhooks.source.jsfiles - Read shared hooks: Loads all
.jsfiles fromshared-hooks/and its subfolders - Concatenate: Combines shared code + component code
- Dead code elimination: Uses esbuild to remove unused functions
- Output: Writes optimized
hooks.jsto each component
Shared hooks are organized into category subfolders:
| Folder | Purpose | Example Functions |
|---|---|---|
core/ |
Essential utilities | classnames, getHoverPrefix, globalHTMLTag |
layout/ |
Layout and positioning | globalLayout, globalActAsGridOrFlexItem |
sizing/ |
Dimensions and aspect ratios | globalSizing, aspectRatioClasses |
spacing/ |
Margin and padding | globalSpacing, globalSpacingMargin |
background/ |
Background styles | globalBackground, globalBgImageFetchPriority |
borders/ |
Borders and outlines | globalBorders, globalOutline |
effects/ |
Visual effects | globalEffects, globalFilters, globalOverlay |
typography/ |
Text and font styles | globalTextFontsAndTextStyles, globalHeadingColor |
transforms/ |
CSS transforms | globalTransforms |
transitions/ |
CSS/Alpine transitions | globalTransitions, getAlpineTransitionAttributesMobile |
animations/ |
Animations and reveals | globalAnimations, globalReveal |
navigation/ |
Navigation styles | globalNavItems, globalMenuItem |
interactive/ |
Links and filters | globalLink, globalFilter |
Each component that needs hooks creates a hooks.source.js file:
// packs/Core.elementsdevpack/components/com.realmacsoftware.button/hooks.source.js
function transformHook(rw) {
// Use any shared hook functions here
const classes = classnames(rw.props.customClasses)
.add(globalSpacing(rw))
.toString();
return {
classes
};
}
exports.transformHook = transformHook;- Entry point: The
transformHookfunction is the only exported function - Dead code elimination: Only code reachable from
transformHookis kept - No manual imports: Shared code is concatenated, not imported
- Auto-generated: Output
hooks.jsfiles are marked "do not edit"
# Build all hooks once
npm run build:hooks
# Watch for changes
npm run build:hooks:watch
# Using npm scripts
npm run build:hooksControls are reusable UI component definitions that map to RapidWeaver's property inspector UI elements.
A control is a JavaScript object (or array of objects) that defines:
// Simple control (single object)
const FlexDirection = {
title: "Direction",
id: "flexDirection",
select: {
default: "flex-col",
items: [
{ value: "flex-col", title: "Column" },
{ value: "flex-row", title: "Row" },
],
},
};
export default FlexDirection;// Compound control (array of objects)
const Link = [
{
title: "Link",
heading: {}
},
{
title: "To",
id: "globalLink",
link: {}
}
];
export default Link;Controls can use these UI element types:
| Type | Description | Example |
|---|---|---|
select |
Dropdown menu | { select: { default: "value", items: [...] } } |
segmented |
Segmented button control | { segmented: { default: "a", items: [...] } } |
switch |
Boolean toggle | { switch: { default: false } } |
slider |
Numeric slider | { slider: { default: 50, min: 0, max: 100 } } |
number |
Numeric input | { number: { default: 0 } } |
text |
Text input | { text: { default: "" } } |
textArea |
Multi-line text | { textArea: { default: "" } } |
link |
Link picker | { link: {} } |
resource |
Resource/image picker | { resource: {} } |
heading |
Section heading (no value) | { heading: {} } |
divider |
Visual separator | { divider: {} } |
information |
Info text | { information: {} } |
themeColor |
Theme color picker | { themeColor: { default: {...} } } |
themeSpacing |
Theme spacing picker | { themeSpacing: { mode: "single" } } |
themeBorderRadius |
Border radius picker | { themeBorderRadius: {...} } |
themeBorderWidth |
Border width picker | { themeBorderWidth: {...} } |
themeShadow |
Shadow picker | { themeShadow: {...} } |
| Property | Type | Description |
|---|---|---|
title |
string | Display label in the UI |
id |
string | Property identifier (used in templates) |
format |
string | CSS class format, e.g., "gap-x-{{value}}" |
visible |
string | Visibility condition, e.g., "otherProp == 'value'" |
responsive |
boolean | Whether control is responsive (default: true) |
globalControl |
string | Reference to another control (nested) |
Controls can reference other controls for composition:
const Borders = [
{
globalControl: "ControlType",
id: "{{value}}Borders",
},
{
globalControl: "BorderStyle",
visible: "globalControlTypeBorders == 'static'",
},
{
globalControl: "BorderColor",
visible: "globalControlTypeBorders == 'static'",
},
];Properties are reusable value definitions (like enums or option lists) that can be referenced using the use key.
// properties/FontWeight.js
const FontWeight = {
default: "normal",
items: [
{ value: "thin", title: "Thin" },
{ value: "light", title: "Light" },
{ value: "normal", title: "Normal" },
{ value: "medium", title: "Medium" },
{ value: "semibold", title: "Semibold" },
{ value: "bold", title: "Bold" },
],
};
export default FontWeight;Reference a property using the use key:
const TextWeight = {
title: "Weight",
id: "textWeight",
select: {
use: "FontWeight" // Merges FontWeight's items and default
}
};The build system will merge the referenced property, with local values taking precedence:
// Output
{
title: "Weight",
id: "textWeight",
select: {
default: "normal",
items: [
{ value: "thin", title: "Thin" },
// ... etc
]
}
}{
"groups": [
{
"title": "Group Title",
"icon": "sf-symbol-name",
"properties": [
// Property definitions
]
}
]
}Reference a control by name:
{
"globalControl": "Spacing"
}Add properties alongside globalControl to override:
{
"globalControl": "BorderRadius",
"title": "Corner Radius",
"default": {
"base": {
"topLeft": "lg",
"topRight": "lg",
"bottomLeft": "none",
"bottomRight": "none"
}
}
}Transform the control's ID using a template:
{
"globalControl": "Spacing",
"id": "card{{value}}"
}If the Spacing control has id: "globalPadding", the output becomes id: "cardGlobalPadding".
Override theme-related properties:
{
"globalControl": "Background_Color",
"themeColor": {
"default": {
"name": "brand",
"brightness": 500
}
}
}Supported theme properties:
themeColorthemeFontthemeBorderRadiusthemeBorderWidththemeSpacingthemeShadowthemeTextStyle
Properties without globalControl are passed through with use resolution:
{
"title": "Custom Width",
"id": "customWidth",
"slider": {
"use": "Slider",
"default": 100,
"min": 0,
"max": 500
}
}Create a new file in the appropriate controls/ subdirectory:
// controls/Effects/NewEffect.js
const NewEffect = {
title: "Effect Intensity",
id: "effectIntensity",
format: "effect-[{{value}}%]",
slider: {
default: 50,
min: 0,
max: 100,
round: true,
units: "%"
}
};
export default NewEffect;Add the export to controls/index.js:
// In the appropriate section
export { default as NewEffect } from "./Effects/NewEffect.js";Reference in any properties.config.json:
{
"globalControl": "NewEffect"
}cd src
npm run buildFor controls with multiple UI elements:
// controls/interactive/CustomButton.js
const CustomButton = [
{
title: "Button Settings",
heading: {}
},
{
title: "Style",
id: "buttonStyle",
segmented: {
default: "solid",
items: [
{ value: "solid", title: "Solid" },
{ value: "outline", title: "Outline" },
{ value: "ghost", title: "Ghost" }
]
}
},
{
title: "Size",
id: "buttonSize",
select: {
use: "ButtonSize"
}
}
];
export default CustomButton;// controls/Layout/CustomLayout.js
const CustomLayout = [
{
globalControl: "ControlType",
id: "{{value}}CustomLayout"
},
{
visible: "globalControlTypeCustomLayout != 'none'",
divider: {}
},
{
visible: "globalControlTypeCustomLayout != 'none'",
globalControl: "Position"
},
{
visible: "globalControlTypeCustomLayout != 'none'",
globalControl: "ZIndex"
}
];
export default CustomLayout;// properties/CustomSizes.js
const CustomSizes = {
default: "md",
items: [
{ value: "xs", title: "Extra Small" },
{ value: "sm", title: "Small" },
{ value: "md", title: "Medium" },
{ value: "lg", title: "Large" },
{ value: "xl", title: "Extra Large" },
]
};
export default CustomSizes;// properties/index.js
export { default as CustomSizes } from "./CustomSizes.js";const SizeSelector = {
title: "Size",
id: "elementSize",
select: {
use: "CustomSizes"
}
};Or use directly in config files:
{
"title": "Size",
"id": "mySize",
"select": {
"use": "CustomSizes"
}
}Create a new file in the appropriate shared-hooks/ subfolder:
// shared-hooks/effects/customEffect.js
/**
* Generate custom effect classes based on element properties
* @param {Object} rw - The RapidWeaver element object
* @returns {string} CSS class string
*/
function customEffect(rw) {
const { customEnabled, customIntensity } = rw.props;
if (!customEnabled) return '';
return classnames([
'custom-effect',
customIntensity && `intensity-${customIntensity}`
]).toString();
}Reference the function in any hooks.source.js:
// packs/MyPack.elementsdevpack/components/com.example.mycomponent/hooks.source.js
function transformHook(rw) {
// customEffect is available from shared hooks (no import needed)
const effectClasses = customEffect(rw);
return {
effectClasses
};
}
exports.transformHook = transformHook;npm run build:hooks- Folder organization: Place files in the appropriate category folder
- Prefix with
global: For element property processing functions (e.g.,globalSpacing) - Use descriptive names: Match the function name to the file name
| Category | Folder | Example |
|---|---|---|
| Core utilities | core/ |
classnames.js, getHoverPrefix.js |
| Layout functions | layout/ |
globalLayout.js |
| Visual effects | effects/ |
globalEffects.js |
| Typography | typography/ |
globalHeadingColor.js |
The build system automatically removes unused code. If you add a function to shared hooks but no component uses it, it won't appear in any output hooks.js file. This keeps the output lean.
// shared-hooks/layout/globalLayout.js
/**
* Generate layout-related CSS classes
*/
const globalLayout = (app, args = {}) => {
const {
globalLayoutPosition: position,
globalLayoutZIndex: zIndex,
globalLayoutOverflow: overflow,
} = app.props;
return classnames([
position,
zIndex,
overflow,
]).toString();
};Show/hide controls based on other property values:
{
"title": "Custom Value",
"id": "customValue",
"visible": "sizeType == 'custom'",
"number": {
"default": 100
}
}Complex conditions:
{
"visible": "enableFeature == true && mode == 'advanced'"
}Generate CSS class names from values:
{
"id": "gapX",
"format": "gap-x-{{value}}",
"themeSpacing": {
"default": { "base": { "value": "4" } }
}
}Output when value is "8": gap-x-8
By default, controls are responsive. Disable with:
{
"id": "staticValue",
"responsive": false,
"text": { "default": "" }
}The build system automatically:
- Injects
CSSClassesandIDcontrols at the start of the Advanced group - Creates an Advanced group if one doesn't exist
- Moves any existing Advanced group to the end
- Sets the icon to "gearshape"
To add controls to the Advanced group:
{
"groups": [
{
"title": "Advanced",
"properties": [
{ "divider": {} },
{ "globalControl": "HTMLTag" }
]
}
]
}# Build everything (properties + hooks)
rw-build all
# Build properties only
rw-build properties
# Build hooks only
rw-build hooks
# Watch for changes
rw-build all --watch # Watch both properties and hooks
rw-build properties --watch # Watch properties only
rw-build hooks --watch # Watch hooks only
# Build with custom packs directory
rw-build all --packs ./my-elements
# Show help
rw-build --helpAdd these to your package.json:
{
"scripts": {
"build": "rw-build all",
"build:properties": "rw-build properties",
"build:hooks": "rw-build hooks",
"dev": "rw-build all --watch"
}
}Then run:
npm run build
npm run devThe --watch flag monitors for changes and automatically rebuilds:
| Command | Watches |
|---|---|
rw-build all --watch |
Both properties and hooks (runs watchers concurrently) |
rw-build properties --watch |
properties.config.json files in packs/ |
rw-build hooks --watch |
hooks.source.js files in packs/ and shared-hooks/*.js |
"Global control 'X' not found"
- Check the control is exported in
controls/index.js - Verify the spelling matches exactly (case-sensitive)
"Property 'X' not found in Properties"
- Check the property is exported in
properties/index.js - Verify the
usekey spelling
Build produces unexpected output
- Check for circular
globalControlreferences - Verify JSON syntax in config files
- Run with
--trace-warningsflag for more details
{
"groups": [
{
"title": "Content",
"icon": "text.alignleft",
"properties": [
{
"title": "Heading",
"id": "headingText",
"text": {
"default": "Welcome"
}
},
{
"globalControl": "HeadingColor"
}
]
},
{
"title": "Layout",
"icon": "square.split.bottomrightquarter",
"properties": [
{
"globalControl": "Layout"
}
]
},
{
"title": "Spacing",
"icon": "squareshape.squareshape.dotted",
"properties": [
{
"globalControl": "Spacing"
}
]
},
{
"title": "Background",
"icon": "paintbrush.fill",
"properties": [
{
"globalControl": "BackgroundTransparent"
}
]
},
{
"title": "Borders",
"icon": "square.dashed",
"properties": [
{
"globalControl": "Borders"
}
]
},
{
"title": "Advanced",
"properties": [
{
"divider": {}
},
{
"globalControl": "HTMLTag"
}
]
}
]
}You can also use rw-elements-tools programmatically in your own build scripts:
import {
buildProperties,
buildHooks,
watchProperties,
watchHooks,
resolveConfig,
Controls,
Properties
} from 'rw-elements-tools';
// Resolve configuration from all sources
const config = await resolveConfig({
packs: './my-elements' // Optional CLI override
});
// Build properties
await buildProperties(config);
// Build hooks
await buildHooks(config);
// Watch for changes
await watchProperties(config); // Watch properties only
await watchHooks(config); // Watch hooks only
// Or watch both concurrently
await Promise.all([
watchProperties(config),
watchHooks(config)
]);import { Controls, Properties } from 'rw-elements-tools';
// Use a control definition
console.log(Controls.Spacing);
console.log(Controls.BorderRadius);
// Use a property definition
console.log(Properties.FontWeight);
console.log(Properties.Slider);import { resolveConfig, buildProperties, buildHooks } from 'rw-elements-tools';
async function customBuild() {
const config = await resolveConfig();
console.log('Building for:', config.packsDir);
// Build properties first
await buildProperties(config);
// Then build hooks
await buildHooks(config);
console.log('Build complete!');
}
customBuild();To publish the package to npm:
cd src
npm login
npm publishFor scoped packages:
npm publish --access publicWhen adding new controls or properties:
- Follow naming conventions: PascalCase for exports, descriptive names
- Place in correct directory: Use the categorical structure
- Add exports: Update the relevant
index.js - Test the build: Run
npm run buildand verify output - Document: Add comments for complex controls
Last updated: January 2026