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

@welshare/questionnaire

v0.3.0

Published

FHIR Questionnaire components for React with state management and validation

Downloads

40

Readme

@welshare/questionnaire

FHIR R5 Questionnaire components for React with state management, validation, and theming.

Installation

npm install @welshare/questionnaire

Quick Start

import { useState, useEffect } from "react";
import {
  QuestionnaireProvider,
  useQuestionnaire,
  QuestionRenderer,
  getVisiblePages,
  type Questionnaire,
} from "@welshare/questionnaire";
import "@welshare/questionnaire/tokens.css";
import "@welshare/questionnaire/styles.css";

function QuestionnairePage() {
  const { questionnaire, isPageValid } = useQuestionnaire();
  const pages = getVisiblePages(questionnaire);
  const [currentPageIndex, setCurrentPageIndex] = useState(0);
  const currentPage = pages[currentPageIndex];

  return (
    <div>
      <h1>{questionnaire.title}</h1>
      {currentPage?.item?.map((item) => (
        <QuestionRenderer key={item.linkId} item={item} />
      ))}
      <button
        onClick={() => setCurrentPageIndex(currentPageIndex + 1)}
        disabled={!isPageValid(currentPage?.item || [])}
      >
        Next
      </button>
    </div>
  );
}

function App() {
  const [questionnaire, setQuestionnaire] = useState<Questionnaire | null>(
    null
  );

  useEffect(() => {
    fetch("/api/questionnaire/your-id")
      .then((res) => res.json())
      .then(setQuestionnaire);
  }, []);

  if (!questionnaire) return <div>Loading...</div>;

  return (
    <QuestionnaireProvider questionnaire={questionnaire}>
      <QuestionnairePage />
    </QuestionnaireProvider>
  );
}

API

QuestionnaireProvider

Props:

  • questionnaire: Questionnaire - FHIR Questionnaire object
  • questionnaireId?: string - Optional ID override (defaults to questionnaire.id)
  • useNestedStructure?: boolean - Nested or flat response structure (default: true)

useQuestionnaire Hook

const {
  questionnaire,
  response,
  updateAnswer,
  updateMultipleAnswers,
  getAnswer,
  getAnswers,
  isPageValid,
  getRequiredQuestions,
  getUnansweredRequiredQuestions,
  markValidationErrors,
  clearValidationErrors,
  hasValidationError,
  debugMode,
  toggleDebugMode,
} = useQuestionnaire();

QuestionRenderer

Props:

  • item: QuestionnaireItem - Questionnaire item to render
  • className?: string - Container CSS classes
  • inputClassName?: string - Input CSS classes
  • choiceClassName?: string - Choice option CSS classes
  • choiceLayout?: ChoiceLayout - "stacked" (default) or "inline-wrap" (horizontal chip layout)
  • renderRadioInput?: (props: RadioInputProps) => ReactNode - Custom radio renderer
  • renderCheckboxInput?: (props: CheckboxInputProps) => ReactNode - Custom checkbox renderer

Supported Types: coding, boolean, integer, decimal, string, text, quantity

Choice Questions with Custom Text Answers

Choice fields can optionally support custom user text input alongside coded options when you set answerConstraint: "optionsOrString" in the FHIR questionnaire definition:

{
  "linkId": "referral-source",
  "text": "How did you hear about us?",
  "type": "coding",
  "answerConstraint": "optionsOrString",
  "answerOption": [
    { "valueCoding": { "code": "search", "display": "Search engine" } },
    { "valueCoding": { "code": "social", "display": "Social media" } }
  ]
}

This renders coded options with an "Other" text field. For single-select (repeats: false), selecting a coded option or entering text is mutually exclusive. For multi-select (repeats: true), coded selections and free text coexist in the answer array.

BmiForm

A controlled component for collecting height, weight, and calculating BMI. The component acts as an input helper that manages BMI calculation internally and reports changes to the parent via callbacks. Unit system (metric/imperial) is managed internally and automatically clears all fields when switched.

Props:

  • height: number - Current height value (0 when empty)
  • weight: number - Current weight value (0 when empty)
  • bmi: number - Current BMI value (calculated and set by the component, 0 when not calculated)
  • onHeightChange: (value: number, unit: "cm" | "in") => void - Called when height changes, includes current unit
  • onWeightChange: (value: number, unit: "kg" | "lb") => void - Called when weight changes, includes current unit
  • onBmiChange: (value: number) => void - Called when BMI is calculated or cleared
  • className?: string - Optional CSS classes

Features:

  • Controlled numeric values (height, weight, bmi) managed by parent
  • BMI calculation handled internally by the component
  • Unit system managed internally (defaults to metric)
  • Automatically clears all fields (sets to 0) when switching unit systems
  • No unit conversion - values are cleared on unit system change
  • Uses consistent styling with questionnaire components

Example:

import { useState } from "react";
import { BmiForm, getBmiCategory } from "@welshare/questionnaire";
import "@welshare/questionnaire/tokens.css";
import "@welshare/questionnaire/styles.css";

function MyComponent() {
  const [height, setHeight] = useState(0);
  const [weight, setWeight] = useState(0);
  const [bmi, setBmi] = useState(0);

  const handleBmiChange = (value: number) => {
    setBmi(value);

    // Optional: Get BMI category
    if (value) {
      const category = getBmiCategory(value);
      console.log(`BMI Category: ${category}`);
    }
  };

  return (
    <BmiForm
      height={height}
      weight={weight}
      bmi={bmi}
      onHeightChange={(value, unit) => setHeight(value)}
      onWeightChange={(value, unit) => setWeight(value)}
      onBmiChange={handleBmiChange}
    />
  );
}

Quantity Questions with Unit Selection

Quantity questions allow users to enter numeric values with unit selection. Use the standard FHIR questionnaire-unitOption extension to define available units:

const waistCircumferenceItem = {
  linkId: "waist",
  type: "quantity",
  text: "What is your waist circumference?",
  required: false,
  code: [
    {
      system: "http://loinc.org",
      code: "8280-0",
      display: "Waist circumference at umbilicus by tape measure",
    },
  ],
  extension: [
    {
      url: "http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption",
      valueCoding: {
        system: "http://unitsofmeasure.org",
        code: "cm",
        display: "cm",
      },
    },
    {
      url: "http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption",
      valueCoding: {
        system: "http://unitsofmeasure.org",
        code: "[in_i]",
        display: "in",
      },
    },
  ],
};

Features:

  • Automatic unit toggle when multiple units are defined
  • Single unit display when only one unit is provided
  • Fallback to simple decimal input when no units are configured
  • Value automatically clears when switching units

Response Format:

{
  "linkId": "waist",
  "answer": [
    {
      "valueQuantity": {
        "value": 85,
        "unit": "cm",
        "system": "http://unitsofmeasure.org",
        "code": "cm"
      }
    }
  ]
}

LegalConsentForm

A self-contained form component for collecting user consent before data submission. This component handles:

  1. Terms & Conditions and Privacy Policy (required) - User must accept to proceed
  2. Study invitation notifications (optional) - User can opt in to receive study invitations

Usage Pattern: This component provides the content for a consent dialog. Applications should wrap this component in their own Dialog/Modal component (e.g., from shadcn/ui, Radix UI, MUI, etc.), similar to how BmiForm is used with input helpers.

Props:

  • initialValues?: Partial<LegalConsentResult> - Initial consent state (for restoring previous preferences)
  • documentLinks?: LegalDocumentLinks - Custom URLs for Terms & Privacy documents
  • onConfirm?: (result: LegalConsentResult) => void - Called when user confirms (only if terms accepted)
  • onCancel?: () => void - Called when user cancels/declines
  • onChange?: (result: LegalConsentResult) => void - Called on any checkbox change (real-time updates)
  • renderCheckbox?: (props: LegalCheckboxProps) => ReactNode - Custom checkbox component renderer
  • confirmButtonLabel?: string - Custom confirm button text (default: "Confirm & Continue")
  • cancelButtonLabel?: string - Custom cancel button text (default: "Cancel")
  • className?: string - Additional CSS classes

Result Object:

interface LegalConsentResult {
  termsAccepted: boolean; // Required - T&Cs and Privacy Policy
  studyConsentAccepted: boolean; // Optional - Study invitations
}

Example with Dialog:

import { useState } from "react";
import {
  LegalConsentForm,
  type LegalConsentResult,
} from "@welshare/questionnaire";
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { Checkbox } from "@/components/ui/checkbox";
import "@welshare/questionnaire/tokens.css";
import "@welshare/questionnaire/styles.css";

function DataSubmissionFlow() {
  const [showConsent, setShowConsent] = useState(false);

  const handleSubmitData = () => {
    // Show consent dialog before submitting data
    setShowConsent(true);
  };

  const handleConsent = (result: LegalConsentResult) => {
    console.log("Terms accepted:", result.termsAccepted);
    console.log("Study consent:", result.studyConsentAccepted);

    // Proceed with data submission
    submitDataToServer(result);
    setShowConsent(false);
  };

  return (
    <>
      <button onClick={handleSubmitData}>Submit My Data</button>

      <Dialog open={showConsent} onOpenChange={setShowConsent}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Health Data Storage Consent</DialogTitle>
          </DialogHeader>
          <LegalConsentForm
            renderCheckbox={({ id, checked, onCheckedChange, className }) => (
              <Checkbox
                id={id}
                checked={checked}
                onCheckedChange={onCheckedChange}
                className={className}
              />
            )}
            onConfirm={handleConsent}
            onCancel={() => setShowConsent(false)}
          />
        </DialogContent>
      </Dialog>
    </>
  );
}

Custom Document Links:

<LegalConsentForm
  documentLinks={{
    termsUrl: "https://myapp.com/terms",
    privacyUrl: "https://myapp.com/privacy",
  }}
  onConfirm={handleConsent}
  onCancel={handleCancel}
/>

CSS Classes for Styling:

  • .wq-legal-consent-form - Main form container
  • .wq-legal-consent-section - Individual consent section
  • .wq-legal-consent-heading - Section headings
  • .wq-legal-consent-checkbox-row - Checkbox + label row
  • .wq-legal-consent-link - Links to legal documents
  • .wq-legal-consent-info-box - Information box (study consent details)
  • .wq-legal-consent-actions - Button container
  • .wq-button, .wq-button-primary, .wq-button-outline - Button styles

Utilities

Questionnaire Utilities:

  • getVisiblePages(questionnaire) - Get visible page groups
  • calculateProgress(currentIndex, total) - Calculate progress percentage
  • getAllQuestionsFromPage(pageItem) - Get all questions from a page

BMI Helper Functions:

  • calculateBmi(height, weight, unitSystem) - Calculate BMI from height and weight
  • getBmiCategory(bmi) - Get WHO BMI category (Underweight, Normal weight, Overweight, Obese)

Theming

Override CSS custom properties:

:root {
  --wq-color-primary-500: #3b82f6;
  --wq-space-lg: 1.5rem;
  --wq-font-size-base: 1rem;
}

Choice Layout

Choice-based questions (coding, choice, boolean) support two layout modes via the choiceLayout prop:

| Mode | Class applied | Behavior | |------|---------------|----------| | "stacked" (default) | .wq-choice-layout-stacked | Vertical column, one option per row | | "inline-wrap" | .wq-choice-layout-inline-wrap | Horizontal chip layout that wraps |

<QuestionRenderer
  item={item}
  choiceLayout="inline-wrap"
/>

Both modes apply to single-select, multi-select (repeats: true), and boolean questions. Existing consumers that don't set choiceLayout see no change (defaults to "stacked").

Chip Design Tokens

When using inline-wrap, these tokens control chip appearance:

| Token | Default | Purpose | |-------|---------|---------| | --wq-choice-chip-radius | 9999px (pill) | Border radius | | --wq-choice-chip-padding-x | 1rem | Horizontal padding | | --wq-choice-chip-padding-y | 0.5rem | Vertical padding | | --wq-choice-chip-gap | 0.5rem | Gap between chips |

CSS Class Contract

The choice container always emits:

  • .wq-question-choice — base container
  • .wq-choice-layout-stacked or .wq-choice-layout-inline-wrap — layout variant
  • Your choiceClassName value (if provided)

State classes on individual options remain unchanged: .wq-selected, .wq-disabled.

Quick Setup: Dark Chip Theme

To get a dark-themed chip-style UI (horizontal wrapping options with strong selected state), combine the layout prop with token overrides and a small scoped CSS file.

1) Render with layout and a shared choice class:

<QuestionRenderer
  item={item}
  choiceLayout="inline-wrap"
  choiceClassName="questionnaire-choice"
/>

2) Set theme tokens (global CSS):

:root {
  --wq-color-surface: hsl(246 65% 10%);
  --wq-color-border: hsl(234 50% 20%);
  --wq-color-border-hover: hsl(214 98% 52%);
  --wq-color-border-focus: hsl(214 98% 52%);
  --wq-color-text-primary: hsl(220 20% 95%);
  --wq-color-text-secondary: hsl(220 15% 60%);
  --wq-color-selected: hsl(214 98% 52%);
  --wq-color-selected-border: hsl(214 98% 52%);

  --wq-choice-chip-radius: 14px;
  --wq-choice-chip-padding-x: 1rem;
  --wq-choice-chip-padding-y: 0.625rem;
  --wq-choice-chip-gap: 0.5rem;
}

3) Add scoped brand overrides (e.g. CustomInputs.css):

.wq-question-choice.questionnaire-choice .wq-choice-option.wq-selected {
  background: var(--wq-color-selected);
  border-color: var(--wq-color-selected-border);
}

.wq-question-choice.questionnaire-choice .wq-choice-label {
  overflow-wrap: anywhere;
}

4) (Optional) Hide native radio/checkbox visuals:

Keep inputs in the DOM for accessibility; hide only the visual markers for pure chips:

.wq-question-choice.questionnaire-choice input[type="radio"],
.wq-question-choice.questionnaire-choice input[type="checkbox"] {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  opacity: 0;
  pointer-events: none;
}

Notes:

  • Keep overrides scoped to your choiceClassName to avoid unintended global changes.
  • Use package state classes (.wq-selected, .wq-disabled) instead of custom state class names.
  • This setup works consistently for single-choice, multi-select, and boolean question types.

Custom Input Renderers

<QuestionRenderer
  item={item}
  renderRadioInput={(props) => (
    <input type="radio" checked={props.checked} onChange={props.onChange} />
  )}
  renderCheckboxInput={(props) => (
    <input type="checkbox" checked={props.checked} onChange={props.onChange} />
  )}
/>

Renderer Props: linkId, valueCoding, valueInteger, checked, disabled, onChange, label, index

FHIR Extensions

Hidden Questions

{
  "extension": [
    {
      "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden",
      "valueBoolean": true
    }
  ]
}

Media Attachments (Images)

Display images or other media content with question items using the FHIR SDC (Structured Data Capture) extensions.

Item Media (images on questions):

{
  "linkId": "body-diagram",
  "text": "Where is the pain located?",
  "type": "choice",
  "extension": [
    {
      "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemMedia",
      "valueAttachment": {
        "contentType": "image/png",
        "url": "https://example.com/images/body-diagram.png",
        "title": "Body diagram"
      }
    }
  ]
}

Answer Option Media (images on answer options):

{
  "linkId": "skin-condition",
  "text": "Which image best matches your condition?",
  "type": "choice",
  "answerOption": [
    {
      "valueCoding": {
        "code": "mild",
        "display": "Mild"
      },
      "extension": [
        {
          "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemAnswerMedia",
          "valueAttachment": {
            "contentType": "image/jpeg",
            "url": "https://example.com/images/mild.jpg",
            "title": "Mild condition"
          }
        }
      ]
    },
    {
      "valueCoding": {
        "code": "severe",
        "display": "Severe"
      },
      "extension": [
        {
          "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemAnswerMedia",
          "valueAttachment": {
            "contentType": "image/jpeg",
            "url": "https://example.com/images/severe.jpg",
            "title": "Severe condition"
          }
        }
      ]
    }
  ]
}

CSS Classes for Styling:

  • .wq-question-media - Container for question-level images
  • .wq-question-image - Individual question image
  • .wq-choice-option-wrapper - Wrapper around answer option with image
  • .wq-choice-option-image - Individual answer option image

Example Styling:

.wq-question-image {
  max-width: 100%;
  height: auto;
  margin-bottom: 1rem;
  border-radius: 0.5rem;
}

.wq-choice-option-image {
  max-width: 200px;
  height: auto;
  margin-bottom: 0.5rem;
  border-radius: 0.25rem;
}

Slider Controls

{
  "extension": [
    {
      "url": "http://codes.welshare.app/StructureDefinition/questionnaire-slider-control",
      "extension": [
        { "url": "minValue", "valueInteger": 0 },
        { "url": "maxValue", "valueInteger": 100 },
        { "url": "step", "valueInteger": 1 },
        { "url": "unit", "valueString": "minutes" }
      ]
    }
  ]
}

Exclusive Options

{
  "extension": [
    {
      "url": "http://codes.welshare.app/StructureDefinition/questionnaire-exclusive-option",
      "valueString": "none-of-the-above-code"
    }
  ]
}

Input Helpers

Input helpers allow you to provide auxiliary UI (like calculators or lookup dialogs) to assist users in filling out form fields. The library detects the extension and calls your renderHelperTrigger function, but you control the dialog/modal implementation.

FHIR Extension:

{
  "linkId": "bmi",
  "text": "Body Mass Index",
  "type": "decimal",
  "extension": [
    {
      "url": "http://codes.welshare.app/StructureDefinition/questionnaire-inputHelper",
      "valueCodeableConcept": {
        "coding": [
          {
            "system": "http://codes.welshare.app/input-helper-type",
            "code": "bmi-calculator",
            "display": "BMI Calculator"
          }
        ],
        "text": "Calculate BMI from height and weight"
      }
    }
  ]
}

Implementation:

import { useState } from "react";
import {
  QuestionRenderer,
  BmiForm,
  type HelperTriggerProps,
} from "@welshare/questionnaire";

function MyQuestionnaireRenderer({ item }) {
  const [showBmiDialog, setShowBmiDialog] = useState(false);
  const [helperCallback, setHelperCallback] = useState<
    ((v: string | number) => void) | null
  >(null);

  const handleHelperTrigger = ({
    helper,
    onValueSelected,
  }: HelperTriggerProps) => {
    if (helper.type === "bmi-calculator") {
      return (
        <button
          type="button"
          onClick={() => {
            setHelperCallback(() => onValueSelected);
            setShowBmiDialog(true);
          }}
        >
          {helper.display || "Calculate"}
        </button>
      );
    }
    return null;
  };

  return (
    <>
      <QuestionRenderer item={item} renderHelperTrigger={handleHelperTrigger} />

      {showBmiDialog && (
        <Dialog onClose={() => setShowBmiDialog(false)}>
          <BmiForm
            onSubmit={({ bmi }) => {
              helperCallback?.(bmi);
              setShowBmiDialog(false);
            }}
          />
        </Dialog>
      )}
    </>
  );
}

Helper Trigger Props:

  • helper: InputHelperConfig - Configuration from the extension
    • type: string - Helper identifier (e.g., "bmi-calculator")
    • display?: string - Display name from the extension
    • description?: string - Description/tooltip text
  • linkId: string - The question's linkId
  • currentValue?: T - Current field value (if any), where T defaults to string | number
  • onValueSelected: (value: T) => void - Callback to update the field

Note: HelperTriggerProps<T> is a generic interface. For decimal questions, it's specialized as HelperTriggerProps<number>, but you can use different types for other question types (e.g., HelperTriggerProps<string> for text inputs).

Note: The library only renders the trigger element you provide. You are responsible for implementing the dialog/modal UI, as this allows you to use your preferred dialog system (e.g., shadcn/ui, Radix UI, MUI, etc.).

License

MIT © Welshare UG (haftungsbeschränkt)