A text input in three heights that match Button, plus InputGroup and InputGroupAddon for leading or trailing icons, units and domains.
import { Input } from "@/components/ballmac/input"
export default function InputDemo() {
return (
<div className="w-full max-w-sm">
<Input type="email" placeholder="Email address" autoComplete="email" aria-label="Email address" />
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/inputInstall the dependencies.
$ pnpm add class-variance-authorityCopy the source into your project.
components/ballmac/input.tsx// Ballmac UI: Input. https://ui.ballmac.com/components/input // Based on shadcn/ui's Input (MIT, Copyright (c) 2023 shadcn), restyled with sizes and an InputGroup for addons. "use client" import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const inputVariants = cva( "flex w-full min-w-0 rounded-md border border-input bg-background text-foreground shadow-xs outline-none transition-[color,border-color,box-shadow] duration-150 selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30", { variants: { size: { sm: "h-8 px-2.5 text-[13px]", default: "h-9 px-3 text-sm", lg: "h-11 px-4 text-[15px]", }, }, defaultVariants: { size: "default" }, } ) type InputProps = Omit<React.ComponentProps<"input">, "size"> & VariantProps<typeof inputVariants> & { /** Native `size` attribute (visible character width). `size` is the height variant. */ htmlSize?: number } function Input({ className, type = "text", size = "default", htmlSize, ...props }: InputProps) { return ( <input type={type} data-slot="input" size={htmlSize} className={cn(inputVariants({ size }), className)} {...props} /> ) } const inputGroupVariants = cva("", { variants: { size: { sm: "h-8 [&>[data-slot=input]]:text-[13px]", default: "h-9", lg: "h-11 [&>[data-slot=input]]:text-[15px]", }, }, defaultVariants: { size: "default" }, }) type InputGroupProps = React.ComponentProps<"div"> & VariantProps<typeof inputGroupVariants> /** * Wraps an Input with leading or trailing addons (icons, units, domains). * The group draws the border, height and focus ring; the inner input goes borderless. */ function InputGroup({ className, size = "default", ...props }: InputGroupProps) { return ( <div data-slot="input-group" role="group" className={cn( inputGroupVariants({ size }), "relative flex w-full min-w-0 items-center rounded-md border border-input bg-background shadow-xs transition-[color,border-color,box-shadow] duration-150 dark:bg-input/30", "has-[[data-slot=input]:focus-visible]:border-ring has-[[data-slot=input]:focus-visible]:ring-[3px] has-[[data-slot=input]:focus-visible]:ring-ring/50", "has-[[data-slot=input][aria-invalid=true]]:border-destructive has-[[data-slot=input][aria-invalid=true]]:ring-destructive/20", "has-[[data-slot=input]:disabled]:opacity-50", "[&>[data-slot=input]]:h-full [&>[data-slot=input]]:flex-1 [&>[data-slot=input]]:border-0 [&>[data-slot=input]]:bg-transparent [&>[data-slot=input]]:shadow-none [&>[data-slot=input]]:ring-0 [&>[data-slot=input]]:focus-visible:ring-0 [&>[data-slot=input]]:dark:bg-transparent", "has-[>[data-align=start]]:[&>[data-slot=input]]:ps-1.5 has-[>[data-align=end]]:[&>[data-slot=input]]:pe-1.5", className )} {...props} /> ) } type InputGroupAddonProps = React.ComponentProps<"div"> & { /** Which side of the input the addon sits on (visual order; keep markup order the same for screen readers). */ align?: "start" | "end" } function InputGroupAddon({ className, align = "start", onMouseDown, ...props }: InputGroupAddonProps) { return ( <div data-slot="input-group-addon" data-align={align} className={cn( "flex h-full shrink-0 select-none items-center gap-1.5 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", align === "start" ? "order-first ps-3" : "order-last pe-3", className )} onMouseDown={(event) => { onMouseDown?.(event) // Clicking an addon focuses the input, like a label would. if (event.defaultPrevented || (event.target as HTMLElement).closest("button, a, input, select, textarea")) return const input = event.currentTarget.parentElement?.querySelector<HTMLInputElement>("[data-slot=input]") if (input) { event.preventDefault() input.focus() } }} {...props} /> ) } export { Input, InputGroup, InputGroupAddon, inputVariants, inputGroupVariants, type InputProps, type InputGroupProps, type InputGroupAddonProps }Update the import paths to match your project setup.
Usage
import { Input, InputGroup, InputGroupAddon } from "@/components/ballmac/input"The full example is in the Code tab above.
Examples
With icon and suffix
import { Search } from "lucide-react"
import { InputGroup, InputGroupAddon, Input } from "@/components/ballmac/input"
import { Kbd, KbdGroup } from "@/components/ballmac/kbd"
export default function InputWithIcon() {
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<InputGroup>
<InputGroupAddon>
<Search aria-hidden="true" />
</InputGroupAddon>
<Input type="search" placeholder="Search documentation" aria-label="Search documentation" />
<InputGroupAddon align="end">
<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>K</Kbd>
</KbdGroup>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupAddon>https://</InputGroupAddon>
<Input placeholder="your-site" aria-label="Domain name" />
<InputGroupAddon align="end">.com</InputGroupAddon>
</InputGroup>
</div>
)
}Invalid
Enter a full email address, like alex@acme.com.
import { Input } from "@/components/ballmac/input"
import { Label } from "@/components/ballmac/label"
export default function InputInvalid() {
return (
<div className="grid w-full max-w-sm gap-2">
<Label htmlFor="invalid-email">Work email</Label>
<Input
id="invalid-email"
type="email"
defaultValue="alex@acme"
aria-invalid="true"
aria-describedby="invalid-email-error"
/>
<p id="invalid-email-error" className="text-sm text-destructive">
Enter a full email address, like alex@acme.com.
</p>
</div>
)
}API reference
<Input>
| Prop | Type | Default |
|---|---|---|
htmlSizeNative `size` attribute (visible character width). `size` is the height variant. | number | — |
size | "sm" | "default" | "lg" | "default" |
<InputGroup>
| Prop | Type | Default |
|---|---|---|
size | "sm" | "default" | "lg" | "default" |
<InputGroupAddon>
| Prop | Type | Default |
|---|---|---|
alignWhich side of the input the addon sits on (visual order; keep markup order the same for screen readers). | "start" | "end" | "start" |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab | Moves focus to the input |
| Click on an addon | Focuses the input |
Use with AI
Single-line text field. Use size to match neighbouring buttons, wrap in InputGroup with InputGroupAddon (align start or end) for icons or text like ".com", and set aria-invalid for error styling. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Input (@ballmac/input) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Any single-line form field (email, name, URL, search)
- Search boxes with a leading icon
- Fields with a fixed unit or domain suffix
Not for
- Multi-line text (use textarea)
- Choosing from a fixed list (use select)
- On/off settings (use switch or checkbox)
Registry JSON: https://ui.ballmac.com/r/input.json
Credits
Based on shadcn/ui Input, adapted by Ballmac. Free to use in personal and commercial projects.
- Registry
- shadcn/utils
Pairs well with
Label
An accessible form label built on Radix Label that dims itself when the paired peer control is disabled and never selects text on double-click.
Button
A button with six variants, three sizes, a pill shape and a built-in loading state. buttonVariants() styles links the same way.
Kbd
Keyboard key and shortcut display built on the semantic <kbd> element, in three sizes, with KbdGroup for combinations like ⌘ ⇧ K. Adapts inside tooltips.
Tooltip
A Radix tooltip with an arrow, side offset and fade and zoom transitions that opens on hover and keyboard focus. Works standalone or under a shared provider.