shadimage
turbopills-ui
componentinputsource: ok · refreshed

Input

Input field with title, hint, suffix and error message.

npx shadcn add "https://ui.turbopills.com/r/input.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Files2 · source cached 10/8/2026, 2:23:17 AM

registry/turbopills/lib/styles.tsx
import { cn } from "@/lib/utils";

export const optionStyles = {
  base: "h-15 inline-flex items-center justify-between w-full bg-background border rounded-lg p-4 cursor-pointer transition-all",
  textSelectionDisabled: "select-none",
  hover: "hover:ring-1 hover:ring-border",
  outlineOnSelect: "ring-2 ring-primary hover:ring-2 hover:ring-primary",
  fillOnSelect: "bg-primary/10",
  shadowOnSelect: "shadow-md shadow-primary/25",
  disabled: "opacity-50 cursor-not-allowed",
};

export const getOptionClassName = ({
  selected = false,
  highlightOnHover = true,
  showOutlineOnSelect = true,
  showFillOnSelect = false,
  showShadowOnSelect = false,
  disabled = false,
  allowTextSelection = false,
}: {
  selected?: boolean;
  highlightOnHover?: boolean;
  showOutlineOnSelect?: boolean;
  showFillOnSelect?: boolean;
  showShadowOnSelect?: boolean;
  disabled?: boolean;
  allowTextSelection?: boolean;
}) =>
  cn(
    optionStyles.base,
    !allowTextSelection && optionStyles.textSelectionDisabled,
    highlightOnHover && optionStyles.hover,
    selected && showOutlineOnSelect && optionStyles.outlineOnSelect,
    selected && showFillOnSelect && optionStyles.fillOnSelect,
    selected && showShadowOnSelect && optionStyles.shadowOnSelect,
    disabled && optionStyles.disabled,
  );

export const fieldHeaderStyles = {
  container: "mb-3 space-y-1",
  title: "text-3xl leading-9 font-medium",
  titleSm: "text-xl leading-6 font-medium",
  hint: "text-muted-foreground",
  hintSm: "text-sm text-muted-foreground",
};

export const inputStyles = {
  wrapper:
    "relative flex items-center w-full border rounded-2xl bg-background hover:border-primary transition-[color,border-color] overflow-hidden focus-within:ring-1 focus-within:ring-primary focus-within:border-primary",
  wrapperDefault: "h-[60px]",
  wrapperSm: "h-10 !rounded-lg",
  input:
    "flex-1 w-full bg-transparent border-0 outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
  inputDefault: "px-5 py-4 text-md placeholder:text-md",
  inputSm:
    "h-[38px] p-2 text-sm font-light placeholder:text-sm placeholder:font-light",
  suffix: "flex items-center pl-2 pr-5 text-muted-foreground text-sm",
  error: "border-destructive focus-within:outline-destructive/20 focus-within:border-destructive",
  autofill:
    "[&:has(input:-webkit-autofill)]:bg-[#e8f0fe]",
  inputAutofill:
    "autofill:[box-shadow:inset_0_0_0_9999px_#e8f0fe] autofill:[-webkit-text-fill-color:var(--color-foreground)]",
};

export const getInputWrapperClassName = ({
  size = "md",
  hasError = false,
}: {
  size?: "md" | "sm";
  hasError?: boolean;
}) =>
  cn(
    inputStyles.wrapper,
    inputStyles.autofill,
    size === "sm" ? inputStyles.wrapperSm : inputStyles.wrapperDefault,
    hasError && inputStyles.error,
  );

export const getInputClassName = ({
  size = "md",
  hasSuffix = false,
}: {
  size?: "md" | "sm";
  hasSuffix?: boolean;
}) =>
  cn(
    inputStyles.input,
    inputStyles.inputAutofill,
    size === "sm" ? inputStyles.inputSm : inputStyles.inputDefault,
    hasSuffix && "pr-0",
  );
registry/turbopills/ui/input.tsx
"use client";

import * as React from "react";

import { cn } from "@/lib/utils";
import {
  fieldHeaderStyles,
  inputStyles,
  getInputWrapperClassName,
  getInputClassName,
} from "@/lib/styles";

export interface InputFieldProps
  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
  title?: string;
  hint?: string;
  size?: "md" | "sm";
  suffix?: React.ReactNode;
  errorMessage?: string;
  wrapperClassName?: string;
}

export const Input = React.forwardRef<HTMLInputElement, InputFieldProps>(
  (
    {
      title,
      hint,
      size = "md",
      suffix,
      errorMessage,
      className,
      wrapperClassName,
      disabled,
      ...props
    },
    ref,
  ) => {
    const inputId = React.useId();

    return (
      <div className="space-y-1">
        {(title || hint) && (
          <div className={fieldHeaderStyles.container}>
            <label htmlFor={inputId}>
              {title && <p className={size === "sm" ? fieldHeaderStyles.titleSm : fieldHeaderStyles.title}>{title}</p>}
              {hint && <p className={size === "sm" ? fieldHeaderStyles.hintSm : fieldHeaderStyles.hint}>{hint}</p>}
            </label>
          </div>
        )}

        <div
          className={cn(
            getInputWrapperClassName({ size, hasError: !!errorMessage }),
            disabled && "opacity-50 cursor-not-allowed",
            wrapperClassName,
          )}
        >
          <input
            ref={ref}
            id={inputId}
            disabled={disabled}
            aria-invalid={!!errorMessage}
            aria-describedby={errorMessage ? `${inputId}-error` : undefined}
            className={cn(
              getInputClassName({ size, hasSuffix: !!suffix }),
              className,
            )}
            {...props}
          />

          {suffix && <div className={inputStyles.suffix}>{suffix}</div>}
        </div>

        {errorMessage && (
          <p
            id={`${inputId}-error`}
            role="alert"
            className="text-sm text-destructive"
          >
            {errorMessage}
          </p>
        )}
      </div>
    );
  },
);

Input.displayName = "Input";

More from turbopills-ui

Browse all components in this library →