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

nuxt-formkit-webform

v0.0.1

Published

Render Drupal Webforms as FormKit forms.

Readme

nuxt-formkit-webform

A Nuxt module that renders Drupal Webforms as FormKit forms, sourcing the form definition from the graphql_webform Drupal module via GraphQL and submitting back through the same schema's mutation.

Requirements

On the Drupal side, the backend must expose a GraphQL endpoint with the webform and graphql_webform modules enabled.

On the Nuxt side, this module builds on two peer modules that you install and configure yourself:

Install

npx nuxt module add nuxt-formkit-webform
npm install nuxt-graphql-middleware @formkit/nuxt

Setup

1. nuxt.config.ts

export default defineNuxtConfig({
  modules: [
    // MUST come before nuxt-graphql-middleware — see below.
    'nuxt-formkit-webform',
    'nuxt-graphql-middleware',
    '@formkit/nuxt',
  ],

  graphqlMiddleware: {
    graphqlEndpoint: 'https://example.com/graphql',
  },

  formkitWebform: {
    // All options are optional — see "Module options".
  },
})

Module order is load-bearing. nuxt-formkit-webform must be listed before nuxt-graphql-middleware: it registers its GraphQL documents via the nuxt-graphql-middleware:init hook, which has already fired if the middleware boots first. The module throws at build time if you get this wrong, so you won't debug it silently.

2. formkit.config.ts

withWebformConfig registers everything the module needs in one call — the custom input types, validation rules, locale messages (EN/DE/FR/IT), and the plugins behind Drupal's label/description affordances:

import { defineFormKitConfig } from '@formkit/vue'
import { withWebformConfig } from 'nuxt-formkit-webform/formkit'
import { rootClasses } from './formkit.theme'

export default defineFormKitConfig(() =>
  withWebformConfig({
    config: { rootClasses },
    // Your own plugins / rules / messages go here. They win on key
    // conflicts; whole locale buckets are deep-merged, not clobbered.
  }),
)

New module features (input types, rules, locales) arrive by version bump without your config moving. Per-piece opt-out lives in nuxt.config (see "FormKit integration options"), and every constituent piece is exported individually if you'd rather assemble by hand.

FormKit integration options

Opt-outs go in nuxt.config, not into withWebformConfig — they're baked into the generated config as literal consts, so turning one off drops its code from the bundle:

formkitWebform: {
  formkit: {
    helpTooltip: false, // `?` affordance (Drupal `#help`)
    more: false, // embedded `<details>` disclosure (Drupal `#more`)
    asterisk: false, // required-marker (`*`)
    counter: false, // live character/word count (Drupal `#counter_*`)
  },
}

All four are presentational: asterisk: false keeps aria-required, counter: false keeps Drupal's server-side limit. more: false only drops the EMBEDDED disclosure — the standalone WebformElementWebformMore element renders its own; exclude it via supportedElements.

3. Theme and CSS

The module ships default Tailwind classes for its custom inputs (webformMessage, webformSection, webformFieldset, webformContainer, webformDetails, webformFlexbox, webformItem, webformDatelist, webformLikert) and for the shared affordances (required asterisk, ? help tooltip, "more" disclosure). Two exports:

| Export | Keyed by | Merge into | | ---------------- | --------------------------- | ---------------------- | | webformClasses | `${type}__${section}` | your theme's classes | | webformGlobals | bare section name | your theme's globals |

If you use stock Tailwind: import them

Merge both maps into the theme file you already have (e.g. a formkit theme --theme=regenesis generated formkit.theme.ts). Keep them in separate Object.assign calls so a theme regen doesn't trample them:

// formkit.theme.ts
import { webformClasses, webformGlobals } from 'nuxt-formkit-webform/formkit'

const classes: Record<string, Record<string, boolean>> = {
  /* generated */
}
Object.assign(classes, webformClasses)

const globals: Record<string, Record<string, boolean>> = {
  /* generated */
}
Object.assign(globals, webformGlobals)

Tailwind will not emit these classes unless you tell it to scan the shipped file. Tailwind scans file text, not imported values, and it ignores node_modules by default — so without this, the classes exist in the DOM but no CSS is generated for them:

/* app/assets/css/main.css */
@import 'tailwindcss';

@source "../../../node_modules/nuxt-formkit-webform/dist/formkit/theme.mjs";

(Adjust the relative path to your CSS file's location.)

If you use your own class names: copy it

The theme is a plain map of section keys to class objects, so there's nothing to subclass — copy src/formkit/theme.ts into your repo, swap the Tailwind classes for your own, and merge your copy instead of the import. The section keys are the contract; the classes are yours. Each input's source file documents the full section list it exposes.

Stylesheets

A handful of behaviours can't be expressed as utility classes. Import only the ones you need:

// nuxt.config.ts
css: [
  // Drupal `#input_hide` parity (value masked until focus).
  'nuxt-formkit-webform/css/input-hide.css',
  // Layout for the range element's `#output` affordance.
  'nuxt-formkit-webform/css/range-output.css',
  // Spacing fix for sortable tableselect.
  'nuxt-formkit-webform/css/webform_tableselect_sort.css',
  // Classes Drupal itself may emit (`.visually-hidden`, …).
  'nuxt-formkit-webform/css/webform-classes.css',
]

Wizard forms (Drupal's webform_wizard_page) render as FormKit's multi-step input. Its stylesheet is FormKit's own — import it straight from @formkit/addons, which you already have as a transitive dependency:

css: ['@formkit/addons/css/multistep']

Rendering a form

<NuxtWebform> takes the fetched webform payload, not an id — so you own the query, and caching / SSR / reactivity stay in your hands. The module registers a webformById query into nuxt-graphql-middleware, so it's already available to useAsyncGraphqlQuery:

<template>
  <NuxtWebform
    v-if="webform"
    :webform="webform"
    @success="onSuccess"
    @error="onError"
  />
</template>

<script setup lang="ts">
const { data: webform } = await useAsyncGraphqlQuery(
  'webformById',
  { id: 'contact' },
  // Unwrap the GraphQL envelope. The native `Webform` shape is passed
  // through as-is — <NuxtWebform> reads `form.elements` itself.
  { transform: (data) => data.data.webformById },
)

function onSuccess(payload) {
  // payload: { submissionId, submissionToken, confirmation, submission }
}
function onError(errors: readonly string[]) {}
</script>

Because the variables are reactive, making sourceEntityId a ref reloads the form against the new context automatically.

Props

| Prop | Type | Description | | ------------------ | ----------------------- | -------------------------------------------------------------------------------------------------------------------- | | webform | WebformPayload | Required. The webformById result, passed through unreshaped. | | sourceEntityType | string | Source entity the submission is attached to. Pass the same value you used in the query. | | sourceEntityId | string | ↑ | | submitLabel | string | Overrides the submit button label. | | sectionsSchema | WebformSectionsSchema | Per-element FormKit section overrides, keyed by element key. Lets you replace any section's markup without a mapper. | | debug | boolean | Dev aid — renders a <details> block with the generated schema + live submission state. |

Events

  • success{ submissionId, submissionToken, confirmation, submission }
  • errorreadonly string[] of form-level errors. Element-level validation errors are pushed back onto the corresponding inputs automatically; you don't need to route them.

Slots

  • unavailable — replaces the default banner when the form can't be filled in (closed, submission limit reached, source entity required/mismatched…). Receives the unavailable payload. <NuxtWebform> renders the form or this — never both.
  • default — rendered after the form.

Query variables worth knowing

webformById accepts sourceEntityType / sourceEntityId (context) and prepopulate — a URL-encoded query string in Drupal's own convention (customer_name=Alice&colors[]=red) that pre-fills element values server-side.

Module options

formkitWebform: {
  // Extra directories to scan for mappers, on top of the convention
  // directory `~/webform/mappers/` (app root + every layer).
  mapperDirs: [],

  // Restrict which element typenames are supported. Pass EITHER include
  // (allow-list) OR exclude (deny-list) — the type forbids both.
  supportedElements: { exclude: ['WebformElementManagedFile'] },

  // Element typename with no mapper: 'warn' (default) | 'skip' | 'throw'.
  onUnknownElement: 'warn',

  // How deep the generated master fragment unrolls container recursion.
  containerRecursionDepth: 4,

  // Mapper discovery / generated fragment logging. Auto-on when Nuxt's
  // `debug` is on.
  debug: false,

  // See "FormKit integration options".
  formkit: {
    pro: { enabled: false },
    helpTooltip: true,
    more: true,
    asterisk: true,
    counter: true,
  },

  // Max options at which the time element renders a <select> instead of
  // <input type="time">. 0 disables the auto-selectify behaviour.
  timeSelectThreshold: 24,
}

Elements outside the configured supportedElements set are skipped silently at runtime — they don't trigger onUnknownElement.

FormKit Pro

Drupal's '#multiple': true elements render through FormKit Pro's repeater. This module does not bundle FormKit Pro. To enable it:

  1. Register @formkit/pro and the repeater input in your own formkit.config.ts.
  2. Set formkitWebform.formkit.pro.enabled = true.

When pro.enabled is false (the default), multi-value elements fall back to single-value rendering with a one-time console warning per element.

Custom elements

To support a Drupal element this module doesn't ship a mapper for — or to override a bundled one — drop a folder into ~/webform/mappers/<Typename>/ (i.e. app/webform/mappers/ in a default Nuxt 4 app). The folder name must match the GraphQL __typename:

app/webform/mappers/WebformElementMyThing/
├── index.ts          # default export: defineWebformElementMapper(...)
└── fragment.graphql  # the fields your mapper needs
// index.ts — defineWebformElementMapper is auto-imported.
export default defineWebformElementMapper(
  'WebformElementMyThing',
  (element) => {
    const meta = element.metadata
    if (!meta) return null

    const node = { $formkit: 'text' }
    applyCommonMetadata(node, meta) // #nuxt-formkit-webform/helpers/*
    return node
  },
)

The fragment is composed into the module's master query automatically, and element is typed from it — no hand-written types. A userland mapper whose typename matches a bundled one overrides it.

Development

This module is developed alongside a DDEV-managed Drupal backend in the graphql-webform-integration repository. See the repo's top-level README.md and CONTRIBUTING.md for the contributor workflow.