nucleusUI
All components

NucleusInput

Inputs

A text field that handles the boring parts for you — label wiring, helper text and a red error state with proper aria attributes.

Preview

We never share it.

That handle is taken.

Installation

  1. 1shadcn/ui initialized (`npx shadcn@latest init`)
  2. 2Tailwind CSS v4 configured with the nucleusUI theme tokens
  3. 3shadcn Input + Label (`npx shadcn@latest add input label`)
npm install lucide-react clsx tailwind-merge

Add to your globals.css

The nucleusUI accent lives in these CSS variables — without them the component falls back to the default shadcn zinc theme.

app/globals.css
:root {
  --primary: oklch(0.54 0.24 291);
  --primary-foreground: oklch(0.985 0 0);
  --ring: oklch(0.54 0.24 291);
  --accent: oklch(0.96 0.03 291);
  --accent-foreground: oklch(0.4 0.19 291);
}

.dark {
  --primary: oklch(0.68 0.21 291);
  --primary-foreground: oklch(0.145 0 0);
  --ring: oklch(0.68 0.21 291);
  --accent: oklch(0.26 0.06 291);
  --accent-foreground: oklch(0.87 0.09 291);
}

Usage

tsx
import { NucleusInput } from "@/components/nucleus/nucleus-input";

export function Example() {
  return (
    <div className="flex w-72 flex-col gap-4">
      <NucleusInput placeholder="Default" />
      <NucleusInput
        label="Email"
        type="email"
        placeholder="you@example.com"
        hint="We never share it."
      />
      <NucleusInput
        label="Workspace"
        placeholder="acme-inc"
        error="That handle is taken."
      />
    </div>
  );
}

Source

Copy this file into components/nucleus/nucleus-input.tsx in your project — it's yours to own and modify.

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

import * as React from "react";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

export interface NucleusInputProps extends React.ComponentProps<"input"> {
  /** Optional label rendered above the field. */
  label?: string;
  /** Helper text shown below the field. */
  hint?: string;
  /** Error message — switches the field into its error state. */
  error?: string;
}

export const NucleusInput = React.forwardRef<HTMLInputElement, NucleusInputProps>(
  function NucleusInput({ className, label, hint, error, id, ...props }, ref) {
    const autoId = React.useId();
    const inputId = id ?? autoId;
    const describedBy = error ? `${inputId}-error` : hint ? `${inputId}-hint` : undefined;

    return (
      <div className="flex w-full flex-col gap-1.5" data-slot="nucleus-input">
        {label && <Label htmlFor={inputId}>{label}</Label>}
        <Input
          ref={ref}
          id={inputId}
          className={cn(
            "h-10 rounded-xl px-3.5 transition-shadow",
            error &&
              "border-destructive aria-invalid:border-destructive focus-visible:ring-destructive/25",
            className
          )}
          aria-invalid={error ? true : undefined}
          aria-describedby={describedBy}
          {...props}
        />
        {error ? (
          <p id={`${inputId}-error`} className="text-xs font-medium text-destructive">
            {error}
          </p>
        ) : hint ? (
          <p id={`${inputId}-hint`} className="text-xs text-muted-foreground">
            {hint}
          </p>
        ) : null}
      </div>
    );
  }
);

Props

PropTypeDescription
labelstringOptional label rendered above the field.
hintstringHelper text shown below the field.
errorstringError message — switches the field into its error state and sets aria-invalid.
...restInputHTMLAttributes<HTMLInputElement>All native input attributes are forwarded.