Skip to content

Documentation

Widget Development

Practical guide to building and testing SveltyCMS widgets.

5/6/2026
2 min read Edit on GitHub

Building widgets for SveltyCMS is a straightforward process using our 3-Pillar system.

๐Ÿš€ Creating a Custom Widget

Create a kebab-case folder in src/widgets/custom/ (same rules as marketplace packages).

Naming (required)

Folder (kebab-case) Factory Name (PascalCase)
phone-number/ Name: "PhoneNumber"
seo/ Name: "SEO"

Invariant: widgetNameToFolder(Name) === folder. Invalid custom widgets are not registered. See Marketplace.

Derived names are title-cased, not acronym-preserving. If a factory omits Name, it is derived from the folder (seo/ โ†’ Seo), so schemas referencing { Name: "SEO" } would miss the registry entry. Always declare Name explicitly โ€” especially for acronyms.

Folder Structure

  • index.ts: Definition (createWidget({ Name: "โ€ฆ" }))
  • input.svelte: Input component
  • display.svelte: Display component
  • tests/: Portable unit tests (Drupal-style)

Try It Live

Open the starter template below in StackBlitz and follow along:

The starter includes a working widget scaffold with the 3-pillar structure, Valibot validation, and pre-configured tests. Click the file tree to explore.

๐Ÿงช Testing

Custom widgets must contain their own tests within a tests/ subdirectory.

// Example: src/widgets/custom/my-widget/tests/my-widget.test.ts
import { describe, it, expect } from "vitest";
import Widget from "../index";
import { safeParse } from "valibot";

describe("MyWidget", () => {
  it("validates data", () => {
    const field = Widget({ label: "Test" });
    const schema = (field.widget.validationSchema as any)(field);
    expect(safeParse(schema, "valid").success).toBe(true);
  });
});

๐Ÿ”’ Multi-Tenant Safety

Widgets are UI components โ€” they render data provided by the server and never query the database directly. However, widget definitions (index.ts) that include server-side hooks or validation must be tenant-aware:

// โœ… Correct: Widget definition accepts tenant context
const MyWidget = createWidget({
  Name: "MyWidget",
  // Server-side validation receives tenant context
  modifyRequest: async ({ data, type, tenantId }) => {
    // tenantId is automatically injected by the widget system
    return data;
  },
});

Key rules for widget developers:

  • Never access fetch() or API routes directly from a widget input/display component
  • Always use the provided value and update props โ€” they maintain tenant context
  • If your widget definition has a modifyRequest hook, always pass tenantId through

Related

widgetsdevelopmentguide
Was this page helpful?