Raqam
Recipes

shadcn/ui

Wrapping raqam with shadcn/ui primitives for a polished, design-system-consistent number input.

shadcn/ui provides styled, accessible components built on Radix UI. Since raqam is headless, it integrates cleanly — just use raqam's hooks/components and apply shadcn's class names.

RaqamInput component

Create a reusable RaqamInput component that follows the shadcn design language:

// components/raqam-input.tsx
"use client";

import * as React from "react";
import { NumberField } from "raqam";
import { cn } from "@/lib/utils";
import type { UseNumberFieldProps } from "raqam";

// UseNumberFieldProps includes every state option plus behavior props
// (label, name, aria-*, copyBehavior, …), so name/aria pass-through type-checks.
interface RaqamInputProps extends UseNumberFieldProps {
  description?: string;
  className?: string;
}

export function RaqamInput({
  label,
  description,
  className,
  ...props
}: RaqamInputProps) {
  return (
    <NumberField.Root
      {...props}
      className={cn("flex flex-col gap-2", className)}
    >
      {label && (
        <NumberField.Label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
          {label}
        </NumberField.Label>
      )}

      <NumberField.Group className="flex h-9 rounded-md border border-input bg-transparent shadow-sm overflow-hidden group-data-[focused]:ring-1 group-data-[focused]:ring-ring group-data-[invalid]:border-destructive">
        <NumberField.Decrement className="px-3 text-muted-foreground hover:text-foreground disabled:opacity-50 border-r border-input bg-muted/30 hover:bg-muted/50 transition-colors select-none text-sm">

        </NumberField.Decrement>

        <NumberField.Input className="flex-1 px-3 py-1 text-sm outline-none bg-transparent placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50" />

        <NumberField.Increment className="px-3 text-muted-foreground hover:text-foreground disabled:opacity-50 border-l border-input bg-muted/30 hover:bg-muted/50 transition-colors select-none text-sm">
          +
        </NumberField.Increment>
      </NumberField.Group>

      {description && (
        <NumberField.Description className="text-xs text-muted-foreground">
          {description}
        </NumberField.Description>
      )}

      <NumberField.ErrorMessage className="text-xs text-destructive" />
    </NumberField.Root>
  );
}

Usage

import { RaqamInput } from "@/components/raqam-input";

// Basic
<RaqamInput
  locale="en-US"
  label="Quantity"
  defaultValue={1}
  minValue={1}
  step={1}
/>

// Currency
<RaqamInput
  locale="en-US"
  label="Price"
  description="Enter the listing price in USD"
  formatOptions={{ style: "currency", currency: "USD" }}
  defaultValue={0}
  minValue={0}
  validate={(v) => v !== null && v > 0 ? true : "Price must be greater than $0"}
/>

Display-only with shadcn Badge

import { Badge } from "@/components/ui/badge";
import { useNumberFieldFormat } from "raqam";

function PriceBadge({ amount }: { amount: number }) {
  const formatted = useNumberFieldFormat(amount, {
    locale: "en-US",
    formatOptions: { style: "currency", currency: "USD" },
  });

  return (
    <Badge variant="secondary" className="font-mono">
      {formatted}
    </Badge>
  );
}

In a shadcn Form (with react-hook-form)

import { useForm } from "react-hook-form";
import { Form, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { NumberField } from "raqam";
import { Controller } from "react-hook-form";

export function ProductForm() {
  const form = useForm({ defaultValues: { price: null as number | null } });

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(console.log)}>
        <FormField
          control={form.control}
          name="price"
          rules={{ required: "Price is required" }}
          render={({ field }) => (
            <FormItem>
              <FormLabel>Price</FormLabel>
              <NumberField.Root
                locale="en-US"
                formatOptions={{ style: "currency", currency: "USD" }}
                value={field.value}
                onChange={field.onChange}
                onBlur={field.onBlur}
                className="w-full"
              >
                <NumberField.Group className="flex h-9 rounded-md border border-input overflow-hidden focus-within:ring-1 focus-within:ring-ring">
                  <NumberField.Decrement className="px-3 bg-muted/30 border-r border-input text-sm text-muted-foreground hover:bg-muted/50">

                  </NumberField.Decrement>
                  <NumberField.Input className="flex-1 px-3 text-sm outline-none bg-transparent" />
                  <NumberField.Increment className="px-3 bg-muted/30 border-l border-input text-sm text-muted-foreground hover:bg-muted/50">
                    +
                  </NumberField.Increment>
                </NumberField.Group>
              </NumberField.Root>
              <FormMessage />
            </FormItem>
          )}
        />
      </form>
    </Form>
  );
}

The cn() utility from shadcn merges Tailwind classes with clsx + tailwind-merge. Import it from @/lib/utils.

On this page