npm package: @satify/widget-sdk
Description: Embed Satify Survey anywhere — popup, slider, popover, inline, or tab
Author: Satify
Latest version: 2.3.0

<div align="center">

# @satify/widget-sdk

**Embed beautiful survey widgets in any website**

[![npm](https://img.shields.io/npm/v/@satify/widget-sdk?color=cb3837&label=npm)](https://www.npmjs.com/package/@satify/widget-sdk)
[![gzip](https://img.shields.io/bundlephobia/minzip/@satify/widget-sdk?label=gzip&color=44cc11)](https://bundlephobia.com/package/@satify/widget-sdk)
[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![license](https://img.shields.io/npm/l/@satify/widget-sdk?color=blue)](./LICENSE)

`popup` · `slider` · `popover` · `inline` · `tab` — 0 dependencies · ~10 KB gzipped · Shadow DOM isolated

</div>

---

## Quick Start

**NPM**

```bash
npm install @satify/widget-sdk
```

```typescript
import { createPopup } from '@satify/widget-sdk'

const widget = createPopup('your-survey-id')
// A floating button appears. Click it → survey opens.
```

**CDN** — no build tools needed

```html
<script src="https://unpkg.com/@satify/widget-sdk@2/dist/cdn.global.js"></script>
<script>
  Satify('popup', 'your-survey-id')
</script>
```

**HTML only** — zero JavaScript

```html
<div data-satify-popup="your-survey-id"></div>
<script src="https://unpkg.com/@satify/widget-sdk@2/dist/cdn.global.js"></script>
```

> **Survey ID?** Dashboard → Surveys → click a survey → Share tab → copy the ID.

---

## Display Formats

| Format | Description | Best for |
|--------|-------------|----------|
| **Popup** | Centered modal with backdrop | NPS, CSAT, general surveys |
| **Slider** | Side panel (left/right) | Long forms, multi-step surveys |
| **Popover** | Small floating card | Quick polls, 1-2 questions |
| **Inline** | Embedded inside your page | Contact pages, help centers |
| **Tab** | Persistent edge tab | Always-available feedback |

```typescript
import { createPopup, createSlider, createPopover, createInline, createTab } from '@satify/widget-sdk'

createPopup('id')
createSlider('id')
createPopover('id')
createInline('id', { inline: { container: '#my-div' } })
createTab('id', { tab: { side: 'right', text: 'Feedback', opensAs: 'slider' } })
```

---

## Triggers & Frequency

Triggers control **when** the survey appears. Frequency controls **how often**.

| Trigger | Config |
|---------|--------|
| **Manual** (default) | User clicks the floating button |
| **Auto delay** | `{ type: 'auto', delay: 5000 }` |
| **Scroll %** | `{ type: 'scroll', scrollPercentage: 50 }` |
| **Exit intent** | `{ type: 'exit-intent' }` (desktop only) |
| **Custom event** | `{ type: 'event', eventName: 'checkout-done' }` |
| **URL match** | `{ type: 'url-match', urlRules: [{ pattern: '/pricing/*' }] }` |

| Frequency | Behavior |
|-----------|----------|
| `once` | Show only once ever |
| `daily` | At most once per day |
| `weekly` | At most once per week |
| `always` | Every time the trigger fires |

```typescript
// Auto-show after 5s, once per week
createPopup('id', {
  trigger: { type: 'auto', delay: 5000, frequency: 'weekly' },
})

// Show on exit intent, only once
createPopover('id', {
  trigger: { type: 'exit-intent', frequency: 'once' },
})

// Show on custom event
createPopup('id', {
  trigger: { type: 'event', eventName: 'checkout-completed', frequency: 'once' },
})
// Then fire it: window.dispatchEvent(new CustomEvent('checkout-completed'))
```

> Frequency is tracked via `localStorage`. No cookies.

---

## Widget API

Every factory function returns a widget instance:

```typescript
const widget = createPopup('survey-id')

widget.open()               // Open the survey
widget.close()              // Close the survey
widget.destroy()            // Remove everything from the DOM
widget.identify(user)       // Attach user info to responses
widget.resetIdentity()      // Clear user identity
widget.on(event, handler)   // Subscribe to events
widget.off(event, handler)  // Unsubscribe
```

> If you call `open()` before initialization finishes, it queues automatically.

---

## Events

```typescript
widget.on('ready', () => {})
widget.on('show', () => {})
widget.on('close', () => {})
widget.on('started', () => {})
widget.on('questionChanged', ({ questionId, index, total }) => {})
widget.on('submit', (responses) => {})
widget.on('error', (error) => {})
widget.on('beforeShow', () => false)  // return false to cancel
```

Or use callbacks in config:

```typescript
createPopup('id', {
  callbacks: {
    onSubmit: (responses) => { /* ... */ },
    onShow: () => { /* ... */ },
    onClose: () => { /* ... */ },
  },
})
```

Both styles work simultaneously.

---

## User Identity

```typescript
widget.identify({
  id: 'user-123',
  email: 'jane@example.com',
  name: 'Jane Doe',
  plan: 'premium',         // any custom properties
})

// Or at creation time:
createPopup('id', { user: { id: 'user-123', email: 'jane@example.com' } })

// Clear on logout:
widget.resetIdentity()
```

---

## Full Configuration

All options are optional. Defaults work great for most cases.

```typescript
createPopup('survey-id', {
  // Trigger
  trigger: {
    type: 'manual',             // 'manual' | 'auto' | 'scroll' | 'exit-intent' | 'event' | 'url-match'
    delay: 5000,                // ms (for 'auto')
    scrollPercentage: 50,       // 0-100 (for 'scroll')
    eventName: '',              // (for 'event')
    urlRules: [],               // (for 'url-match')
    frequency: 'always',        // 'once' | 'daily' | 'weekly' | 'always'
    cooldownMs: 0,
  },

  // Floating button
  triggerButton: {
    show: true,                 // false = headless mode
    text: 'Feedback',
    icon: '💬',
    position: 'bottom-right',   // 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
  },

  // Appearance
  appearance: {
    primaryColor: '#6366f1',
    theme: 'light',             // 'light' | 'dark'
    borderRadius: 12,
    zIndex: 999999,
  },

  // Format-specific (only the matching section is used)
  popup:   { size: 'medium', closeOnOverlay: true, closeOnEscape: true, showCloseButton: true },
  slider:  { side: 'right', width: 400, overlay: true },
  popover: { position: 'bottom-right', width: 380, height: 520 },
  inline:  { container: '#my-div' },
  tab:     { side: 'right', text: 'Feedback', icon: '💬', opensAs: 'popup' },

  // Identity & behavior
  user: { id: 'user-123', email: 'jane@example.com' },
  testMode: false,              // true = ignore frequency limits (dev only)
  autoCloseDelay: 0,            // ms after submission to auto-close (0 = manual)

  // Callbacks (alternative to widget.on())
  callbacks: {
    onReady: () => {},
    onBeforeShow: () => {},     // return false to prevent opening
    onShow: () => {},
    onClose: () => {},
    onStarted: () => {},
    onQuestionChanged: ({ questionId, index, total }) => {},
    onSubmit: (responses) => {},
    onError: (error) => {},
  },
})
```

---

## Examples

### Persistent feedback button

```typescript
createPopup('nps-survey', {
  trigger: { type: 'auto', delay: 10000, frequency: 'weekly' },
  user: { id: currentUser.id, email: currentUser.email },
})
```

### Post-purchase (one-time, no button)

```typescript
const widget = createPopup('post-purchase', { triggerButton: { show: false } })
widget.open()
widget.on('submit', () => { widget.close(); widget.destroy() })
widget.on('close', () => widget.destroy())
```

### React integration

```tsx
import { useEffect, useRef } from 'react'
import { createPopup, type WidgetInstance } from '@satify/widget-sdk'

function FeedbackWidget({ surveyId, user }) {
  const ref = useRef<WidgetInstance | null>(null)

  useEffect(() => {
    ref.current = createPopup(surveyId, {
      trigger: { type: 'auto', delay: 5000, frequency: 'weekly' },
    })
    if (user) ref.current.identify({ id: user.id, email: user.email })
    return () => ref.current?.destroy()
  }, [surveyId])

  return null
}
```

### Vue integration

```vue
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { createPopup } from '@satify/widget-sdk'

const props = defineProps(['surveyId'])
let widget = null

onMounted(() => {
  widget = createPopup(props.surveyId, {
    trigger: { type: 'auto', delay: 5000, frequency: 'weekly' },
  })
})
onUnmounted(() => widget?.destroy())
</script>
```

### Google Analytics tracking

```typescript
const widget = createPopup('survey-id')
widget.on('show', () => gtag('event', 'survey_shown', { survey: 'survey-id' }))
widget.on('submit', (r) => gtag('event', 'survey_completed', { questions: r.length }))
```

---

## CDN Commands

```html
<script src="https://unpkg.com/@satify/widget-sdk@2/dist/cdn.global.js"></script>
<script>
  // Create
  Satify('popup', 'survey-id', { /* config */ })
  Satify('slider', 'survey-id')
  Satify('inline', 'survey-id', { inline: { container: '#div' } })

  // Control
  Satify('open')              // open last created widget
  Satify('close')
  Satify('destroy')
  Satify('identify', { id: 'user-1', email: 'jane@example.com' })
  Satify('resetIdentity')
  Satify('event', 'my-event') // fire custom trigger
  Satify('test', true)        // enable test mode
</script>
```

### HTML Auto-Init

```html
<div data-satify-popup="survey-id"></div>
<div data-satify-slider="survey-id" data-satify-trigger="auto:5000"></div>
<div data-satify-popup="survey-id" data-satify-trigger="exit-intent" data-satify-theme="dark" data-satify-color="#e11d48"></div>
<div data-satify-inline="survey-id"></div>
```

| Trigger attribute | Effect |
|-------------------|--------|
| `auto:5000` | Show after 5 seconds |
| `scroll:75` | Show at 75% scroll |
| `exit-intent` | Show on exit intent |
| `event:my-event` | Show on custom event |

---

## Troubleshooting

| Problem | Solution |
|---------|----------|
| Widget doesn't appear | Check survey ID is correct and survey is ACTIVE. Try `testMode: true`. Check browser console for errors. |
| Blocked by CSP | Add to your CSP: `frame-src https://satify.humancreativelab.cl; connect-src https://satify.humancreativelab.cl` |
| Widget behind other elements | Increase `appearance.zIndex` |
| Events not firing | Call `widget.on()` before `widget.open()`. Event names are case-sensitive. |
| Shows too often / not enough | Adjust `trigger.frequency`. Use `testMode: true` for development. |

---

## Browser Support

Chrome 66+ · Firefox 63+ · Safari 12.1+ · Edge 79+ · Opera 53+ · Samsung Internet 9+

**Required APIs:** Shadow DOM, Custom Elements, AbortController, MutationObserver

**IE is not supported.**

---

## TypeScript

Full type definitions included. No `@types/` package needed.

```typescript
import { createPopup } from '@satify/widget-sdk'
import type { WidgetConfig, WidgetInstance, QuestionResponse, FormatType } from '@satify/widget-sdk'
```

All events, configs, and responses are fully typed.

---

## Architecture

- **Shadow DOM** — widget CSS is completely isolated from your site
- **Iframe bridge** — survey form runs in an iframe with `postMessage` origin validation
- **No-op fallback** — if anything fails, all methods silently no-op (no crashes)
- **Tree-shakeable** — import only what you need
- **Zero dependencies** — no external libraries

---

## Accessibility

- ARIA attributes on all interactive elements
- Focus trap inside modals
- Keyboard navigation (Enter/Space to open, Escape to close)
- Reduced motion support

---

## FAQ

**Multiple surveys on one page?** Yes. The SDK coordinates them — only one opens at a time.

**Works with SPAs?** Yes. URL-match triggers detect route changes automatically.

**Customize survey appearance?** Questions and styling are configured in the Satify dashboard. The SDK controls the container (popup, slider, etc.).

**Works offline?** No. The SDK needs the Satify API to load surveys and submit responses.

**Remove "Powered by Satify"?** Paid plans can hide the badge in dashboard settings.

---

## License

MIT © [Satify](https://satify.com)