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

@citolab/numflux

v1.4.2

Published

Framework-agnostic numpad

Readme

Numflux

Framework-agnostic numpad component

A TypeScript library that provides a clean, extensible numpad implementation. Built agnostically, it works seamlessly with React, Vue, Angular, vanilla JS, or any other framework.

📺 View Live Demo

Table of Contents

Features

  • Pure Core - Side-effect-free reducer for numpad interactions
  • Configurable - Decimal places, validation, theming, custom separators, custom icon integration
  • Framework Agnostic - Works with React, Vue, Angular, Svelte, vanilla JS, or any other framework
  • Zero Dependencies - Tiny bundle size, no runtime dependencies
  • Flexible Styling - Use default styles, CSS Modules, custom CSS, tailwind, or whatever you like
  • Accessible - WCAG 2.1 AA compliant with screen reader support

Quick Start

npm install @citolab/numflux
import { createNumpad } from "@citolab/numflux";
import "@citolab/numflux/dist/style.css";

const numpad = createNumpad(document.getElementById("numpad"), {
  allowDecimal: 2,
  theme: "light", // or "dark"
  onChange: (state, display) => {
    console.log("Value:", display.numeric);
  }
});

Basic Usage

import {
  NumpadAction,
  NumpadState,
  createNumpadState,
  formatDisplayValue,
  mapKeyToAction,
  reduceNumpad
} from "@citolab/numflux";

const config = {
  allowDecimal: 2,
  maxDigits: 8,
  min: 0,
  max: 9999.99,
  sync: false
};

let state: NumpadState = createNumpadState("", config);

const dispatch = (action: NumpadAction) => {
  state = reduceNumpad(state, action, config);
  return formatDisplayValue(state, config);
};

dispatch({ type: "digit", digit: 4 });
dispatch({ type: "digit", digit: 2 });
dispatch({ type: "decimal" });
dispatch({ type: "digit", digit: 5 });
// -> state.value === "42.5"

Configuration Options:

const currencyConfig: NumpadConfig = {
  allowDecimal: 2,         // Exactly 2 decimal places
  allowNegative: false,    // No negative values
  minDigits: 1,            // Minimum digits required
  maxDigits: 8,            // Maximum total digits
  minValue: 0,             // Minimum numeric value
  maxValue: 99999.99,      // Maximum numeric value
  decimalSeparator: ".",   // Decimal separator
  sync: true               // Real-time onChange callbacks
};

Utility Functions:

import { toNumber, isValidValue, sanitizeValue } from "@citolab/numflux";

// Convert to number safely
const num = toNumber("42.50"); // -> 42.5

// Validate against constraints
const isValid = isValidValue("150", { max: 100 }); // -> false

// Clean user input
const clean = sanitizeValue("00042.500", { allowDecimal: 2 }); // -> "42.50"

Styled Numpad

import { createNumpad } from "@citolab/numflux";
import "@citolab/numflux/dist/style.css";

const numpad = createNumpad(document.getElementById("container"), {
  allowDecimal: 2,
  theme: "dark",
  onChange: (state, display) => {
    console.log("Value:", display.numeric);
  },
  onSubmit: (state, display) => {
    console.log("Submitted:", display.formatted);
  }
});

numpad.dispatch({ type: "clear" });
numpad.dispatch({ type: "set", value: "99.99" });
console.log("Current:", numpad.getState().value);
numpad.destroy();

Framework-agnostic DOM (No styling)

import { createNumpadDom } from "@citolab/numflux";

const numpad = createNumpadDom(document.getElementById("container"), {
  allowDecimal: 2,
  theme: {
    name: "custom",
    cssVars: {
      "--numpad-bg": "#1a1a1a",
      "--numpad-text": "#ffffff"
    }
  },
  className: "my-custom-numpad",
  onChange: (state, display) => {
    console.log("Value:", display.numeric);
  }
});

// Access DOM elements directly
numpad.root;    // Main container
numpad.display; // Display element
numpad.keypad;  // Keypad container

Styling Options

Numflux offers two styling approaches:

1. Styled Numpad (Recommended)

Pre-built styles with themes, minimal setup required.

import { createNumpad } from "@citolab/numflux";
import "@citolab/numflux/dist/style.css";

const numpad = createNumpad(container, {
  theme: "dark", // 'light' | 'dark'
  className: "my-numpad" // Additional CSS classes
});

Features: Built-in themes, CSS variable overrides Best for: Most projects


2. Custom Styling (Full Control)

Unstyled core, build your own design with utilities.

import { createNumpadDom, withTheme, withClassNames } from "@citolab/numflux";

let numpad = createNumpadDom(container);
numpad = withTheme(numpad, {
  cssVars: { "--nf-accent": "blue" }
});
numpad = withClassNames(numpad, {
  container: "rounded-lg shadow-xl" // Tailwind, etc.
});

Features: Zero styling, composable utilities, framework integration Best for: Custom designs, CSS frameworks (Tailwind, etc.), full control


Mask Syntax

Numflux supports masked input formats for structured values (decimals, fractions, prefixed/suffixed numbers).

Common mask patterns

  • ___ — simple integer with three slots
  • __,__ — decimal with two integer and two fractional slots
  • __/_ — fraction with a two-digit numerator and one-digit denominator
  • € ___,__ — currency with prefix and decimal slots
  • € __.___,__ — currency with thousands separator and decimal slots
  • __ / __ / ____ — segmented values like dates

Using masks

import { createNumpad } from "@citolab/numflux";

const numpad = createNumpad(container, {
  mask: "€ ___,__",
  onChange: (state, display) => {
    // state.value holds the raw numeric string without prefix/suffix
    console.log("Raw:", state.value);
    console.log("Formatted:", display.formatted);
  }
});

Behavior

  • Slots (_) enforce length and order; numpad auto-advances segments.
  • Prefixes/suffixes are preserved in the display but excluded from state.value.
  • Completion can be checked with isMaskComplete(maskState, maskFormat) from @citolab/numflux.

Utilities available

  • parseMask(maskString) — validate and produce a mask format
  • createMaskState(maskFormat, initialValue) — seed mask state
  • formatMaskValue(maskState, maskFormat) — formatted display string
  • getMaskRawValue(maskState, maskFormat) — numeric string without prefix/suffix

Custom CSS Variables

All approaches support CSS variable customization:

.my-numpad {
  --nf-surface: #1a1a2e;
  --nf-text: #ffffff;
  --nf-accent: #64ffda;
  --nf-button-radius: 12px;
  --nf-font-family: "Inter";
}

Accessibility Options

Numflux is WCAG 2.1 AA compliant with comprehensive accessibility features:

const numpad = createNumpad(container, {
  // Accessibility configuration
  a11y: {
    label: "Price input calculator",           // Component name
    description: "Enter price with decimal",  // Purpose description
    announceChanges: true                     // Screen reader announcements
  }
});

Built-in Accessibility Features:

  • 🎯 ARIA Labels - Descriptive labels for all buttons and regions
  • 📢 Live Announcements - Screen reader feedback for value changes
  • ⌨️ Keyboard Navigation - Full keyboard support with focus management
  • 🎨 Focus Indicators - High-contrast focus outlines
  • 🏗️ Semantic HTML - Proper roles and ARIA attributes

Framework Integration

import { useEffect, useRef } from "react";
import { createNumpad } from "@citolab/numflux";
import "@citolab/numflux/dist/style.css";

function useNumpad(options) {
  const containerRef = useRef(null);
  const instanceRef = useRef(null);

  useEffect(() => {
    if (containerRef.current) {
      instanceRef.current = createNumpad(containerRef.current, options);
    }
    return () => instanceRef.current?.destroy();
  }, []);

  return { containerRef, instance: instanceRef.current };
}

function Calculator() {
  const { containerRef } = useNumpad({
    allowDecimal: 2,
    theme: "dark",
    onChange: (state, display) => {
      console.log("Value:", display.numeric);
    }
  });

  return <div ref={containerRef} />;
}

Custom Hook (Unstyled):

import { useEffect, useRef } from "react";
import { createNumpadDom } from "@citolab/numflux";

function useNumpad(options) {
  const containerRef = useRef(null);
  const instanceRef = useRef(null);

  useEffect(() => {
    if (containerRef.current) {
      instanceRef.current = createNumpadDom(containerRef.current, options);
    }
    return () => instanceRef.current?.destroy();
  }, []);

  return { containerRef, instance: instanceRef.current };
}

Pure React Implementation:

import { useState } from "react";
import { createNumpadState, reduceNumpad, formatDisplayValue } from "@citolab/numflux";

function ReactNumpad() {
  const [state, setState] = useState(() => createNumpadState(""));
  const config = { allowDecimal: 2 };

  const dispatch = (action) => {
    setState(current => reduceNumpad(current, action, config));
  };

  const display = formatDisplayValue(state, config);

  return (
    <div className="numpad">
      <div className="display">{display.formatted}</div>
      <button onClick={() => dispatch({ type: "digit", digit: 1 })}>1</button>
      {/* ... more buttons ... */}
    </div>
  );
}
<template>
  <div ref="numpadRef"></div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { createNumpad } from '@citolab/numflux';
import '@citolab/numflux/dist/style.css';

const props = defineProps(['options']);
const emit = defineEmits(['change']);

const numpadRef = ref(null);
let numpadInstance = null;

onMounted(() => {
  if (numpadRef.value) {
    numpadInstance = createNumpad(numpadRef.value, {
      theme: 'light',
      ...props.options,
      onChange: (state, display) => {
        emit('change', { state, display });
      }
    });
  }
});

onUnmounted(() => {
  numpadInstance?.destroy();
});
</script>
import { Component, ElementRef, Input, Output, EventEmitter, AfterViewInit, OnDestroy } from '@angular/core';
import { createNumpad, CreateNumpadOptions } from '@citolab/numflux';
import '@citolab/numflux/dist/style.css';

@Component({
  selector: 'app-numpad',
  template: '<div></div>'
})
export class NumpadComponent implements AfterViewInit, OnDestroy {
  @Input() options: CreateNumpadOptions = {};
  @Output() change = new EventEmitter();

  private numpadInstance: any;

  constructor(private elementRef: ElementRef) {}

  ngAfterViewInit() {
    this.numpadInstance = createNumpad(
      this.elementRef.nativeElement.firstChild,
      {
        theme: 'light',
        ...this.options,
        onChange: (state, display) => {
          this.change.emit({ state, display });
        }
      }
    );
  }

  ngOnDestroy() {
    this.numpadInstance?.destroy();
  }
}
<script>
  import { onMount, onDestroy } from 'svelte';
  import { createNumpad } from '@citolab/numflux';
  import '@citolab/numflux/dist/style.css';

  export let options = {};
  export let theme = 'light';

  let container;
  let numpadInstance;

  onMount(() => {
    if (container) {
      numpadInstance = createNumpad(container, {
        theme,
        ...options,
        onChange: (state, display) => {
          // Handle changes
          console.log('Value:', display.numeric);
        }
      });
    }
  });

  onDestroy(() => {
    numpadInstance?.destroy();
  });
</script>

<div bind:this={container}></div>

Tailwind CSS:

import { createNumpadDom, withClassNames } from "@citolab/numflux";

function createTailwindNumpad(container, options) {
  const numpad = createNumpadDom(container, options);

  return withClassNames(numpad, {
    container: "bg-white rounded-lg shadow-md p-4",
    display: "text-2xl font-mono text-right bg-gray-100 p-3 rounded mb-4",
    keypad: "grid grid-cols-4 gap-2",
    button: "bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded transition",
    buttonAccent: "bg-green-500 hover:bg-green-600",
    buttonGhost: "bg-gray-300 hover:bg-gray-400 text-black"
  });
}

Styled Components:

import { createNumpadDom, withTheme } from "@citolab/numflux";

function createCustomNumpad(container, theme, options) {
  const numpad = createNumpadDom(container, options);

  return withTheme(numpad, {
    cssVars: {
      "--nf-surface": theme.colors.background,
      "--nf-text": theme.colors.text,
      "--nf-accent": theme.colors.primary
    }
  });
}

Cookbook

📚 View Full Cookbook

Advanced Usage

Create custom integrations using composable utilities:

import {
  createNumpadDom,
  withClassNames,
  withTheme,
  withEventHandlers,
  withAttributes,
  compose
} from "@citolab/numflux";

// Compose multiple enhancements
const createCustomNumpad = compose(
  (numpad) => withTheme(numpad, {
    cssVars: { "--nf-accent": "#ff4757" }
  }),
  (numpad) => withClassNames(numpad, {
    container: "my-numpad",
    button: "my-button"
  }),
  (numpad) => withEventHandlers(numpad, {
    onFocus: () => console.log("Focused!"),
    onBlur: () => console.log("Blurred!")
  }),
  (numpad) => withAttributes(numpad, {
    container: { "data-testid": "numpad" }
  })
);

const numpad = createCustomNumpad(
  createNumpadDom(container, options)
);

Available Utilities:

  • withClassNames() - Apply CSS classes
  • withTheme() - Apply theme variables
  • withEventHandlers() - Add event listeners
  • withAttributes() - Set HTML attributes
  • compose() - Combine multiple utilities
import { createNumpad } from "@citolab/numflux";
import "@citolab/numflux/dist/style.css";

const numpad = createNumpad(container, {
  allowDecimal: 2,
  min: 0,
  max: 1000000,
  keyValidator: (key, currentValue, config) => {
    // Custom validation logic
    if (key === "5" && currentValue.includes("5")) {
      return false; // Don't allow multiple 5s
    }
    return true;
  },
  displayRule: (value, config) => {
    // Custom formatting
    const num = parseFloat(value) || 0;
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD'
    }).format(num);
  }
});
import { createNumpad } from "@citolab/numflux";
import "@citolab/numflux/dist/style.css";

const numpad = createNumpad(container, {
  sync: true, // Enable real-time onChange
  onChange: (state, display) => {
    // Called on every keystroke
    updateUI(display.formatted);
  },
  onSubmit: (state, display) => {
    // Called only on submit
    submitForm(display.numeric);
  }
});

API Reference

// Pure state management
import {
  createNumpadState,
  reduceNumpad,
  formatDisplayValue,
  mapKeyToAction,
  normalizeConfig
} from "@citolab/numflux";

// DOM implementations
import {
  createNumpad,       // Styled numpad (requires CSS import)
  createNumpadDom     // Framework-agnostic DOM (unstyled)
} from "@citolab/numflux";

// Composable utilities
import {
  withClassNames,
  withTheme,
  withEventHandlers,
  withAttributes,
  compose
} from "@citolab/numflux";

// Icon integration helpers
import {
  createSvgIconTheme,
  createCssIconTheme,
  createCustomIconTheme,
  extractSvgString
} from "@citolab/numflux";

// Validation utilities
import {
  toNumber,
  isValidValue,
  sanitizeValue,
  getDecimalPlaces
} from "@citolab/numflux";
interface NumpadConfig {
  // Input validation
  allowDecimal?: boolean | number;  // true, false, or max decimal places
  allowNegative?: boolean;          // Allow negative numbers
  maxDigits?: number | null;        // Maximum total digits
  min?: number | null;              // Minimum value
  max?: number | null;              // Maximum value

  // Formatting
  decimalSeparator?: "." | ",";     // Decimal separator character

  // Behavior
  sync?: boolean;                   // Real-time onChange vs submit-only

  // Accessibility
  a11y?: {
    label?: string;                 // Component accessible name
    description?: string;           // Component description
    announceChanges?: boolean;      // Screen reader announcements (default: true)
  };

  // Custom validation & formatting
  keyValidator?: (key: string, value: string, config: NumpadConfig) => boolean;
  displayRule?: (value: string, config: NumpadConfig) => string;
}
// Available actions
type NumpadAction =
  | { type: "digit"; digit: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 }
  | { type: "decimal" }
  | { type: "delete" }
  | { type: "clear" }
  | { type: "submit" }
  | { type: "toggle-sign" }
  | { type: "set"; value: string };

// State structure
interface NumpadState {
  value: string;        // Raw string value
  cursorPos: number;    // Cursor position
  submitted: boolean;   // Whether value was submitted
}

// Display value
interface DisplayValue {
  raw: string;          // Raw string value
  formatted: string;    // Formatted for display
  numeric: number | null; // Parsed number or null if invalid
}

Contributing

# Clone and install
git clone https://github.com/your-org/numflux.git
cd numflux
npm install

# Development commands
npm run dev          # Start development mode
npm run test         # Run tests
npm run test:watch   # Run tests in watch mode
npm run typecheck    # Type checking
npm run lint         # Lint code
npm run build        # Build for production
npm run storybook    # Start Storybook playground
  • Unit Tests: Comprehensive test suite with 100+ tests
  • Integration Tests: Real DOM testing with jsdom
  • Type Tests: Full TypeScript coverage
  • Visual Tests: Storybook for component testing
npm test                   # Run all tests
npm run test:coverage      # Generate coverage report
npm run test:ui            # Open Vitest UI
src/
├── core/                  # Pure logic & DOM implementation
│   ├── numpad.ts         # State management
│   └── numpad-dom.ts     # Framework-agnostic DOM
├── integrations/         # Framework & styling integrations
│   ├── vanilla.ts        # CSS Modules integration

│   ├── icons.ts          # Icon integration helpers
│   └── utils.ts          # Composable utilities
├── utils/                # Validation & utility functions
├── types/                # TypeScript definitions
└── styles/               # CSS modules & themes


Which Integration Should I Use?

| Integration | Best For | Bundle Size | CSS Import Required | |-------------|----------|-------------|-------------------| | createNumpad | Most projects - Styled numpad with themes | ~8kb gzipped | ✅ Yes | | createNumpadDom | Custom styling, framework integration | ~6kb gzipped | ✅ Yes (your styles) |


Bundle Size: 6-12kb gzipped (depending on integration) Dependencies: Zero Browser Support: Modern browsers (ES2019+)