Styled text input with label and description, built on the @foldkit/ui Input helper.
Installation
Add this component to your project:
pnpm dlx shadcn@latest add @foldcn/inputSource
The component ships as plain source — no build step, no wrapper. Copy it and make it yours.
registry/default/ui/input.ts89 linesimport { Input as FoldkitInput } from '@foldkit/ui'
import type { Html, HtmlBuilder } from 'foldkit/html'
import { cn } from '@/lib/utils'
export const inputClass =
'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm aria-disabled:pointer-events-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50 data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 aria-disabled:bg-input/50 data-disabled:bg-input/50 dark:aria-disabled:bg-input/80 dark:data-disabled:bg-input/80 w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50'
/** Same string as the `label` item's component classes (upstream label.tsx). */
/** Upstream string re-keyed for foldkit: the label precedes the control, so
* upstream's native peer-disabled sibling variant can never match; disabled
* state flows from the wrapper (group/field + data-disabled, mirroring
* switch.ts). */
export const inputLabelClass =
'gap-2 text-sm leading-none font-medium group-data-[disabled]:opacity-50 flex items-center select-none group-data-[disabled]/field:pointer-events-none group-data-[disabled]/field:cursor-not-allowed group-data-[disabled]/field:opacity-50'
export const inputDescriptionClass = 'text-sm text-muted-foreground'
export const inputWrapperClass = 'group/field flex flex-col gap-1.5 w-full'
export type InputConfig<M> = Readonly<{
id: string
label: string
description?: string
onInput?: (value: string) => M
value?: string
isDisabled?: boolean
isReadOnly?: boolean
isInvalid?: boolean
isAutofocus?: boolean
name?: string
type?: string
placeholder?: string
className?: string
labelClass?: string
descriptionClass?: string
wrapperClass?: string
}>
/** Styled text input with label and optional description, built on the
* @foldkit/ui Input helper. */
export const input = <M>(config: InputConfig<M>, h: HtmlBuilder<M>): Html =>
FoldkitInput.view<M>(
{
id: config.id,
onInput: config.onInput,
value: config.value,
isDisabled: config.isDisabled,
isReadOnly: config.isReadOnly,
isInvalid: config.isInvalid,
isAutofocus: config.isAutofocus,
name: config.name,
type: config.type,
placeholder: config.placeholder,
toView: (attributes) =>
h.div(
[
h.Class(cn(inputWrapperClass, config.wrapperClass)),
...(config.isDisabled ? [h.DataAttribute('disabled', '')] : []),
],
[
h.label(
[
...attributes.label,
h.DataAttribute('slot', 'label'),
h.Class(cn(inputLabelClass, config.labelClass)),
],
[config.label],
),
h.input([
...attributes.input,
h.DataAttribute('slot', 'input'),
h.Class(cn(inputClass, config.className)),
]),
config.description === undefined
? h.empty
: h.span(
[
...attributes.description,
h.Class(cn(inputDescriptionClass, config.descriptionClass)),
],
[config.description],
),
],
),
},
h,
)