@qds.dev/base
v0.14.3
Published
Create a Qwik library
Readme
@qds.dev/base
Shared utilities and bindings system for Qwik Design System
Overview
Building reactive components requires consistent patterns for state management, prop handling, and signal/value normalization. Rolling your own leads to inconsistency and bugs across components.
@qds.dev/base provides the bindings system (bind:* props), shared hooks, state utilities, and type definitions that power all QDS components. It handles signal/value normalization so you can pass either value or bind:value to any QDS component without worrying about the difference.
This package is the shared layer of QDS. While you can use it directly when building your own components, you'll typically interact with it indirectly through @qds.dev/ui, @qds.dev/motion, or @qds.dev/code.
Installation
npm install @qds.dev/basePeer dependencies:
@qwik.dev/core
Quick Start
Using the bindings hook
The most common use case is normalizing signal/value props in your components:
import { useBindings } from "@qds.dev/base";
import { component$, type Signal } from "@qwik.dev/core";
// now typed color and bind:color props
type MyProps = BindableProps<{ color: string }>;
// handles value or bind:value passed to it. Add a new property each time to bind
export const MyColor = component$<MyProps>((props) => {
const { colorSig: color } = useBindings(props, { value: "purple" });
return (
<div>
{/* initially, purple, can change both internally and externally */}
{color.value}
</div>
);
});API
@qds.dev/base provides utilities through multiple entry points:
| Entry Point | Exports |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Main (.) | Re-exports all symbols from /state, /helpers, /hooks, /playground, /types |
| /state | useBindings, BindableProps, SignalResults, destructureBindings, useBoundSignal, mergeRefs, useStoreSignal |
| /helpers | appendId, removeId, toggleId, hasIdInList, getNextEnabledIndex, getPrevEnabledIndex, hashString, createCache, Cache, createSymbolName |
| /hooks | useDebouncer, useResumed$, useResumedQrl, useMountTask$, useMountTaskQrl |
| /playground | usePlayground, livePropsContextId, PlaygroundEvent, LivePropsContext (internal - for playground/REPL) |
| /types | addWindowEventListener, removeWindowEventListener |
You can import from main (.) or specific subpath. Both work:
import { useBindings } from '@qds.dev/base'import { useBindings } from '@qds.dev/base/state'
For Contributors: When to Use Each Export
Quick answers to "I need to..."
State Management
| Need | Use | From |
| ----------------------------------------- | --------------------- | -------- |
| Normalize value/signal props in component | useBindings | /state |
| Two-way bind to optional signal | useBoundSignal | /state |
| Remove bind:* props before spreading | destructureBindings | /state |
| Combine multiple refs into one | mergeRefs | /state |
| Create signal from store property | useStoreSignal | /state |
ARIA and Accessibility
| Need | Use | From |
| ------------------------------------ | ------------- | ---------- |
| Add ID to aria-describedby list | appendId | /helpers |
| Remove ID from aria-describedby list | removeId | /helpers |
| Toggle ID in aria-describedby list | toggleId | /helpers |
| Check if ID in space-separated list | hasIdInList | /helpers |
Keyboard Navigation
| Need | Use | From |
| ---------------------------------- | --------------------- | ---------- |
| Find next enabled item in list | getNextEnabledIndex | /helpers |
| Find previous enabled item in list | getPrevEnabledIndex | /helpers |
Lifecycle Hooks
| Need | Use | From |
| ----------------------------------- | --------------- | -------- |
| Debounce function calls | useDebouncer | /hooks |
| Run code when component resumes | useResumed$ | /hooks |
| Run code once on mount with cleanup | useMountTask$ | /hooks |
Utilities
| Need | Use | From |
| --------------------------------------------- | --------------------------- | ---------- |
| Hash string to short ID | hashString | /helpers |
| Create LRU cache | createCache | /helpers |
| Generate Qwik-compatible symbol name | createSymbolName | /helpers |
| Add window event listener (QRL-compatible) | addWindowEventListener | /types |
| Remove window event listener (QRL-compatible) | removeWindowEventListener | /types |
Playground (Internal)
| Need | Use | From |
| ------------------------------------ | -------------------- | ------------- |
| Build interactive component examples | usePlayground | /playground |
| Access live props context | livePropsContextId | /playground |
Note: /playground exports are for internal QDS documentation. Most consumers won't need them.
Usage Examples
destructureBindings - Remove bind:* props before spreading
import { destructureBindings, useBindings } from "@qds.dev/base/state";
import { component$ } from "@qwik.dev/core";
export const Input = component$((props) => {
const { valueSig } = useBindings(props, { value: "" });
const remaining = destructureBindings(props, { value: "" });
return <input {...remaining} value={valueSig.value} />;
});appendId/removeId - Manage ARIA relationships
import { appendId, removeId } from "@qds.dev/base/helpers";
import { useMountTask$ } from "@qds.dev/base/hooks";
import { component$, type PropsOf, Slot, useContext, useSignal } from "@qwik.dev/core";
import { Render } from "../render/render";
import { checkboxContextId } from "./checkbox-context";
type PublicCheckboxDescriptionProps = PropsOf<"div">;
/** A component that renders the description text for a checkbox */
export const CheckboxDescription = component$((props: PublicCheckboxDescriptionProps) => {
const context = useContext(checkboxContextId);
const descriptionId = `${context.localId}-description`;
const descriptionRef = useSignal<HTMLDivElement>();
useMountTask$(({ cleanup }) => {
context.describedByIds.value = appendId(context.describedByIds.value, descriptionId);
cleanup(() => {
context.describedByIds.value = removeId(
context.describedByIds.value,
descriptionId
);
});
}, descriptionRef);
return (
// Identifier for the checkbox description element
<Render
fallback="div"
id={descriptionId}
ui-qds-checkbox-description
{...props}
internalRef={descriptionRef}
>
<Slot />
</Render>
);
});getNextEnabledIndex - Keyboard navigation in lists
import { getNextEnabledIndex } from "@qds.dev/base/helpers";
import { component$, useSignal, useContext } from "@qwik.dev/core";
import { checkboxContextId } from "./checkbox-context";
export const Demo = component$(() => {
const context = useContext(checkboxContextId);
const focusedIndex = useSignal(0);
const handleArrowDown = $(() => {
const nextIndex = getNextEnabledIndex(context.numItems, focusedIndex.value);
if (nextIndex !== -1) focusedIndex.value = nextIndex;
});
return <button onKeyDown$={handleArrowDown}>{/* ... */}</button>;
});See detailed API below for function signatures and complete documentation.
Detailed API Reference
/state - State Management
useBindings
Normalizes value and signal props into a consistent signal interface.
function useBindings<T extends object>(
props: BindableProps<T>,
initialValues: T
): SignalResults<T>;Parameters:
props- Component props with value or bind:value variantsinitialValues- Default values for each bindable property
Returns: Object with signals (suffixed with Sig) for each property
Example:
const { valueSig: value, disabledSig: isDisabled } = useBindings(props, {
value: "",
disabled: false
});destructureBindings
Removes bindable props from props object before spreading to DOM elements.
function destructureBindings<T extends object, Props extends BindableProps<T>>(
props: Props,
initialValues: T
): Omit<Props, keyof T | `bind:${keyof T}`>;Use when: You need to spread remaining props to a DOM element after extracting bindings.
useBoundSignal
Creates a signal bound to an optional external signal.
function useBoundSignal<T>(
givenSignal?: Signal<T>,
initialValue?: T,
valueBasedSignal?: Signal<T | undefined>
): Signal<T>;Use when: You need simpler two-way binding than full useBindings.
mergeRefs
Combines multiple refs into a single ref function.
function mergeRefs<T extends Element>(
...refs: (
| Signal<Element | undefined>
| Signal<T | undefined>
| ((el: T) => void)
| undefined
)[]
): (el: T) => void;Use when: Component needs both internal ref and forwarded ref.
useStoreSignal
Creates a signal from a store property with two-way synchronization.
function useStoreSignal<T>(
store: Record<string, unknown>,
key: keyof typeof store
): Signal<T>;Use when: You need a signal that stays synchronized with a Qwik store property.
/helpers - DOM Utilities
appendId
Appends an ID to a space-separated string of IDs, avoiding duplicates.
function appendId(ids: string | undefined | null, idToAdd: string): string;Use for: Managing aria-describedby, aria-labelledby, or similar attributes.
removeId
Removes an ID from a space-separated string of IDs.
function removeId(ids: string | undefined | null, idToRemove: string): string | undefined;Returns: Updated string, or undefined if result is empty.
toggleId
Toggles an ID in a space-separated string of IDs.
function toggleId(ids: string | undefined | null, idToToggle: string): string | undefined;Use for: Toggling error messages or help text visibility.
hasIdInList
Checks if an ID exists in a space-separated list.
function hasIdInList(idList: string | undefined, targetId: string): boolean;getNextEnabledIndex
Finds the next enabled item in a list.
function getNextEnabledIndex<
E extends { disabled?: boolean } = HTMLButtonElement
>(options: {
items: ItemWithPotentialDisabledRef<E>[];
currentIndex: number;
loop?: boolean; // defaults to true
}): number;Returns: Index of next enabled item, or -1 if none found.
Use for: Arrow key navigation in menus, tabs, listboxes.
getPrevEnabledIndex
Finds the previous enabled item in a list.
function getPrevEnabledIndex<
E extends { disabled?: boolean } = HTMLButtonElement
>(options: {
items: ItemWithPotentialDisabledRef<E>[];
currentIndex: number;
loop?: boolean; // defaults to true
}): number;Returns: Index of previous enabled item, or -1 if none found.
hashString
Generates a fast, non-cryptographic hash from a string.
function hashString(str: string): string;Returns: Base-36 encoded hash string.
Use for: Generating short IDs, cache keys, or DOM element IDs.
createCache
Creates an LRU cache with a maximum size.
function createCache<T>(maxSize: number = 100): Cache<T>;
type Cache<T> = {
get(key: string): T | undefined;
set(key: string, value: T): void;
has(key: string): boolean;
clear(): void;
size: number;
};Use for: Memoizing expensive computations or DOM references.
createSymbolName
Generates a Qwik-compatible symbol name from a string.
function createSymbolName(input: string): string;Returns: Symbol name in format s_ followed by 11 hex characters.
Use for: Creating symbols that integrate with Qwik's QRL system.
/hooks - Lifecycle Hooks
useDebouncer
Creates a debounced function that delays invocation.
function useDebouncer(
fn: QRL<(args: any) => void>,
delay: number
): QRL<(args?: any) => void>;Use for: Debouncing search input, resize handlers, or scroll events.
useResumed$
Runs code when component resumes (on client after interaction or navigation).
function useResumed$(callback: () => void): void;Use for: Adding event listeners, starting animations, or initializing browser-only features.
useResumedQrl
QRL variant of useResumed$. Optimizer transforms useResumed$ into useResumedQrl.
function useResumedQrl(qrl: QRL<() => void>): void;useMountTask$
Runs code once on mount with optional cleanup function.
function useMountTask$(
taskFn: (ctx: { cleanup: (cleanupFn: () => void) => void }) => void,
elementRef: Signal<Element | undefined>
): void;Use for: Setting up subscriptions, intervals, or event listeners that need cleanup.
useMountTaskQrl
QRL variant of useMountTask$. Optimizer transforms useMountTask$ into useMountTaskQrl.
function useMountTaskQrl(
taskFn: QRL<(ctx: { cleanup: (cleanupFn: () => void) => void }) => void>,
elementRef: Signal<Element | undefined>
): void;/types - Window Event Helpers
addWindowEventListener
Adds a window event listener with QRL handler support.
function addWindowEventListener<E extends Event = Event>(
type: string,
handler: QRL<(event: E) => void | Promise<void>>,
options?: boolean | AddEventListenerOptions
): void;Use when: You need to conditionally add window event listeners (e.g., drag handlers).
removeWindowEventListener
Removes a window event listener with QRL handler support.
function removeWindowEventListener<E extends Event = Event>(
type: string,
handler: QRL<(event: E) => void | Promise<void>>,
options?: boolean | EventListenerOptions
): void;/playground - Playground (Internal)
usePlayground
Creates an interactive playground context for component examples.
function usePlayground<T extends Record<string, unknown>>(
/* parameters omitted - internal API */
): /* returns omitted - internal API */Note: This is for internal QDS documentation. Most consumers won't need it.
livePropsContextId
Context ID for accessing live props in playground.
const livePropsContextId: ContextId<LivePropsContext>;Note: Internal use only.
Architecture
For package internals, dependency relationships, and design decisions (including the useBindings pattern rationale), see ARCHITECTURE.md.
Why This Pattern
The bindings system
QDS components accept props like bind:checked alongside standard checked props. This pattern lets you:
- Pass values directly:
<Checkbox checked={true} /> - Pass signals for reactivity:
<Checkbox bind:checked={mySignal} /> - Let components handle it: The component uses
useBindingsto normalize both cases into a consistent internal API
This eliminates the controlled/uncontrolled component distinction from React. You don't need separate value/defaultValue props or onChange callbacks. The bindings system handles synchronization automatically.
Why base is separate
@qds.dev/base is not part of @qds.dev/ui because it provides foundation utilities that can be used independently:
- @qds.dev/ui - Uses
@qds.dev/baseas a devDependency (bundled at build time)
By keeping shared utilities in @qds.dev/base, we enable consistent state management patterns and allow consumers to use the bindings system independently if needed.
Related Packages
Depends on: None (this is the foundation package)
Used by:
- @qds.dev/ui - Headless UI components (devDependency - bundled at build time)
