npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@codebards/ik-embeddable-form

v1.0.7

Published

Production-ready config-driven embeddable form platform with Shadow DOM isolation

Readme

IK Embeddable Form

A production-ready, config-driven embeddable form platform built with Preact, TypeScript, Vite, Tailwind CSS, and Shadow DOM isolation.


Table of Contents

  1. Architecture Overview
  2. Installation
  3. Script Usage
  4. Config Schema
  5. Environment Setup
  6. Build & Deployment
  7. Versioning Strategy
  8. Adding New Form Variants
  9. Analytics Integration
  10. API Integration

Architecture Overview

src/
├── core/
│   ├── FormEngine/       # Orchestrates step transitions, state, API calls
│   ├── StepResolver/     # Evaluates conditions, resolves next steps
│   ├── ConfigResolver/   # Resolves FormConfig from OpenConfig + variants
│   ├── ApiClient/        # HTTP client with retries, body/response mapping
│   └── Analytics/        # GTM · Sentry · Clarity · Cookie facades
│
├── components/
│   ├── Modal/            # Accessible modal with backdrop + progress bar
│   ├── FormSteps/        # StepRenderer · AutoStep · SuccessStep · FieldRenderer
│   └── Shared/           # Button · Input · Select · Textarea · Radio · Checkbox
│
├── configs/
│   ├── sample-webinar.config.ts   # Multi-step webinar registration
│   └── sample-contact.config.ts  # Conditional contact form
│
├── hooks/
│   ├── useFormEngine.ts   # Subscribes to FormEngine state
│   └── useAnalytics.ts    # Analytics action hooks
│
├── styles/
│   └── base.css           # Tailwind directives + Shadow DOM resets
│
├── types/
│   └── index.ts           # FormConfig · FormStep · ValidationRule · ApiContract · AnalyticsEvent
│
├── App.tsx                # Root Preact component
└── sdk/
    ├── index.ts           # window.IKForm global API
    └── mount.ts           # Shadow DOM mount/unmount/destroy

Shadow DOM Isolation

The form is mounted inside a Shadow DOM attached to a <div id="ik-form-host"> injected into document.body. This means:

  • Global CSS on the host page cannot bleed into the form
  • Form styles cannot leak out to the host page
  • No class name or selector conflicts

Installation

Via CDN (Recommended)

<script src="https://cdn.example.com/forms/v1/embed.js"></script>

Self-hosted

# Build for production
npm run build:production

# Upload dist/embed.js to your CDN / server

npm (for framework integration)

npm install ik-embeddable-form
import { IKForm } from 'ik-embeddable-form';

IKForm.open({ eventName: 'my-webinar', webinarType: 'live', ... });

Script Usage

Basic

<script src="https://cdn.example.com/forms/embed.js"></script>
<script>
  window.IKForm.open({
    eventName:     'ai-summit-2026',
    webinarType:   'live',
    site:          'main',
    variant:       'default',
  });
</script>

With All Options

window.IKForm.open({
  // Required
  eventName:     'ai-summit-2026',   // Matches FormConfig.eventName
  webinarType:   'live',             // Matches FormConfig.webinarType
  site:          'main',             // Multi-site identifier (forwarded to API)
  variant:       'compact',          // Must match a FormVariant.id (or 'default')

  // Optional
  preferredSlot:   'slot-morning',   // Pre-selects a slot field

  prefilledValues: {                 // Pre-fills form fields by name
    email:     '[email protected]',
    firstName: 'Jane',
  },

  configOverrides: {                 // Runtime overrides merged on top of config
    theme: { modalMaxWidth: '600px' },
  },

  // Callbacks
  onSuccess: (result) => {
    console.log('Form submitted:', result.data);
    console.log('Ticket/Reg ID:', result.data.registrationId);
  },
  onClose: () => {
    console.log('Modal closed');
  },
  onError: (err) => {
    console.error('Form error:', err.code, err.message);
  },
});

Programmatic Control

// Close the modal
window.IKForm.close();

// Full cleanup (SPA route change)
window.IKForm.destroy();

// Get loaded version
console.log(window.IKForm.getVersion()); // "1.0.0"

Config Schema

FormConfig (root)

| Property | Type | Required | Description | |------------------|-------------------|----------|--------------------------------------------------| | id | string | ✅ | Unique config identifier | | name | string | ✅ | Human-readable form name | | version | string | ✅ | Semver (e.g. "1.2.0") | | eventName | string | ✅ | Matches OpenConfig.eventName | | webinarType | string | – | Matches OpenConfig.webinarType | | defaultVariant | string | ✅ | Fallback variant ID | | variants | FormVariant[] | – | Named overrides applied on top of base config | | steps | FormStep[] | ✅ | Ordered step definitions | | api | ApiContract | ✅ | Default submission endpoint | | analytics | AnalyticsConfig | – | Open/close/submit event config | | theme | ThemeConfig | – | Visual customisation | | i18n | Record<string, string> | – | Localisation strings | | metadata | Record<string, string \| number \| boolean> | – | Forwarded to analytics |

FormStep

| Property | Type | Description | |------------------|----------------------------------|--------------------------------------------------| | id | string | Unique step identifier | | type | "form" \| "auto" \| "success" \| "error" \| "info" | Step type | | title | string? | Step heading | | subtitle | string? | Step sub-heading | | hidden | boolean? | Hide from progress indicator | | condition | ConditionalRule? | Step only reachable when condition passes | | autoExecute | AutoExecuteConfig? | API call fired automatically on step entry | | fields | FormField[]? | Input fields rendered in this step | | api | ApiContract? | Per-step API call (fires on Next) | | nextStep | string \| ConditionalNextStep[]? | Fixed or conditional next step routing | | allowBack | boolean? | Show Back button (default: true) | | submitLabel | string? | Override CTA label | | analyticsEvents| AnalyticsEvent[]? | Events fired when step becomes active |

FormField

| Property | Type | Description | |----------------|-------------------|--------------------------------------------------| | name | string | Field name (key in form data) | | type | FieldType | text \| email \| tel \| number \| textarea \| select \| radio \| checkbox \| date \| hidden \| slot-picker | | label | string? | Label text | | placeholder | string? | Placeholder text | | defaultValue | string \| number \| boolean? | Initial value | | options | FieldOption[]? | Options for select/radio | | validation | ValidationRule[]? | Array of validation rules | | condition | ConditionalRule? | Show/hide field based on other field values | | colSpan | number? | Grid column span (1 = half, 2 = full) |

ValidationRule

| Property | Type | Description | |--------------|------------------|---------------------------------------------------| | type | ValidationType | required \| email \| phone \| min \| max \| minLength \| maxLength \| pattern \| custom \| async | | value | number \| string? | Threshold or pattern string | | message | string | Error message shown to user | | condition | ConditionalRule? | Only apply this rule when condition is true |

ApiContract

| Property | Type | Description | |-------------------|-----------------------|--------------------------------------------------| | endpoint | string | Relative path or full URL. Supports {fieldName} interpolation | | method | HttpMethod | GET \| POST \| PUT \| PATCH \| DELETE | | bodyMapping | Record<string,string> \| "$all" | Map form fields → request body keys | | responseMapping | Record<string,string>? | Map response JSON paths → form field names | | queryParams | Record<string,string>? | Query params with {fieldName} interpolation| | authenticated | boolean? | Include Authorization: Bearer header | | retries | number? | Retry count (default: 1) | | timeoutMs | number? | Request timeout (default: 10,000ms) | | mockResponse | unknown? | Used in local env instead of real network call |

ConditionalRule

{
  logical?: "AND" | "OR",  // default: "AND"
  clauses: [
    {
      field:    "fieldName",        // dot notation supported: "address.city"
      operator: "equals" | "not_equals" | "contains" | "not_contains" |
                "starts_with" | "ends_with" | "greater_than" | "less_than" |
                "is_empty" | "is_not_empty" | "matches_regex",
      value?:   "some value"        // not needed for is_empty / is_not_empty
    }
  ]
}

Environment Setup

| File | Used For | |-------------------|------------------------| | .env | Local development | | .env.staging | Staging builds | | .env.production | Production builds |

Required Variables

VITE_API_BASE_URL=https://api.example.com/api
VITE_GTM_ID=GTM-XXXXXXX
VITE_SENTRY_DSN=https://[email protected]/xxx
VITE_CLARITY_ID=xxxxxxxxxx

Build & Deployment

Local Dev Server

npm install
npm run dev
# Opens http://localhost:3000 with a test page

Build Commands

npm run build            # Builds with .env (local defaults)
npm run build:staging    # Builds with .env.staging
npm run build:production # Builds with .env.production (minified)

Build Output

dist/
├── embed.js        # Self-contained IIFE bundle (reference in <script>)
└── embed.js.map    # Source map (only in staging/local builds)

Deployment Checklist

  1. Run npm run build:production

  2. Upload dist/embed.js to your CDN with a versioned path:

    https://cdn.example.com/forms/v1.0.0/embed.js
  3. Set cache headers:

    • embed.jsCache-Control: public, max-age=31536000, immutable (versioned path)
    • Use a latest alias for convenience: cdn.example.com/forms/latest/embed.js with shorter TTL
  4. Reference on the host page:

    <script src="https://cdn.example.com/forms/v1.0.0/embed.js" async></script>

Versioning Strategy

This project follows Semantic Versioning (MAJOR.MINOR.PATCH):

| Change Type | Version Bump | Example | |----------------------|-------------|---------| | Breaking API change | MAJOR | 2.0.0 | | New form variant | MINOR | 1.1.0 | | Bug fix / patch | PATCH | 1.0.1 |

Release Process

# 1. Bump version in package.json
npm version patch   # or minor / major

# 2. Build production artefact
npm run build:production

# 3. Tag the release
git tag v$(node -p "require('./package.json').version")
git push --tags

# 4. Upload dist/embed.js to CDN under versioned path

The __VERSION__ constant is automatically injected from package.json at build time.


Adding New Form Variants

Option A — New Variant on Existing Config

Add a FormVariant to the variants array of an existing FormConfig:

// src/configs/sample-webinar.config.ts
variants: [
  {
    id: 'enterprise',
    overrides: {
      steps: [ /* override any step */ ],
      theme: { modalMaxWidth: '680px' },
    },
  },
],

Then call:

IKForm.open({ eventName: 'ai-summit-2026', webinarType: 'live', variant: 'enterprise' });

Option B — New FormConfig File

  1. Create src/configs/my-new-form.config.ts
  2. Export a FormConfig object with a unique eventName + webinarType
  3. Register it in src/configs/index.ts:
import { myNewFormConfig } from './my-new-form.config';

export const ALL_CONFIGS: FormConfig[] = [
  webinarConfig,
  contactConfig,
  myNewFormConfig, // ← add here
];

No other files need to change. The platform supports 25+ variants this way.


Analytics Integration

GTM

Events are pushed to window.dataLayer automatically. Configure your GTM triggers on:

  • form_open — fired when modal opens
  • form_close — fired when modal closes
  • form_step_view — fired on each step
  • form_step_complete — fired after Next/Submit per step
  • form_submit_success — fired on final submission
  • form_submit_error — fired on submission failure

Sentry

Install @sentry/browser and uncomment the placeholder code in src/core/Analytics/sentry.ts. The DSN is injected from VITE_SENTRY_DSN.

Clarity

Ensure the Clarity script is on the host page, or uncomment the injection block in src/core/Analytics/clarity.ts.

Custom Events

Listen on the host page:

window.addEventListener('ikform:analytics', (e) => {
  console.log('IKForm event:', e.detail);
});

API Integration

All API calls are defined in ApiContract objects inside your form config — no hardcoded fetch calls.

Dynamic URL Interpolation

endpoint: 'slots/{slot}/reserve'
// If form data has { slot: "slot-morning" }
// → POST https://api.example.com/api/slots/slot-morning/reserve

Body Mapping

bodyMapping: {
  email:    'user.email',      // form field → nested request body key
  slot:     'booking.slotId',
}
// → { user: { email: "..." }, booking: { slotId: "..." } }

// OR forward all fields:
bodyMapping: '$all'

Response Mapping

responseMapping: {
  'data.registrationId': 'registrationId',  // response path → form field
}
// Writes response.data.registrationId back into form state as "registrationId"

Auth Token

import { apiClient } from 'ik-embeddable-form';

apiClient.setAuthToken('Bearer your-token-here');

TypeScript Types Reference

import type {
  FormConfig,
  FormStep,
  FormField,
  ValidationRule,
  ApiContract,
  AnalyticsEvent,
  OpenConfig,
  ConditionalRule,
  IKFormSDK,
} from 'ik-embeddable-form';

License

MIT — see LICENSE