Skip to content
Drivn
7 min read

Drivn vs shadcn/ui — Input Component Compared

Compare Drivn Input vs shadcn/ui — minimal styles.base wrapper with h-10 rounded-[10px] versus shadcn's richer recipe with file: utilities and aria-invalid.

shadcn/ui's Input and Drivn's Input both ship as plain React wrappers around a native <input> element, with no runtime UI dependencies on either side — no Radix, no Floating UI, no input library underneath. The two diverge in what they bake into the default class string. shadcn's Input packs file-input utilities, a focus-visible ring, an aria-invalid destructive state, dark-mode background handling, and a selection:bg-primary selection color into one long cn() call. Drivn's Input keeps the surface minimal: a single styles.base object with h-10, rounded-[10px], px-4, text-sm, and a disabled:opacity-50 disabled:cursor-default pair. The full registry source is about 25 lines of TypeScript.

The prop shape is identical on both sides because it is just React.InputHTMLAttributes<HTMLInputElement> — type, placeholder, value, onChange, disabled, ref, and every other native input attribute. Where the two components diverge is style ergonomics. shadcn lays down close to fifteen utility groups inline. Drivn moves every utility into a styles.base constant that lives above the component, so changing the border-radius, the padding, or the disabled opacity is a one-line edit in a labeled config object instead of a hunt inside a long string. The h-10 and rounded-[10px] choices favor a slightly larger touch target and a softer corner radius than shadcn's h-9 and rounded-md defaults.

This page walks through every difference: dependency footprint, API parity, the focus-state model, what each component bakes in versus what it delegates to consumer styling, and how to extend either one toward error states or icon adornments.

Side-by-side comparison

FeatureDrivnshadcn/ui
Underlying primitiveNative <input> + cn()Native <input> + cn()
Runtime UI dependenciesZeroZero
Ref forwardingReact.forwardRefPlain function (React 19 ref-as-prop)
Default heighth-10 (40px)h-9 (36px)
Default radiusrounded-[10px]rounded-md (6px)
Default paddingpx-4px-3 py-1
Default font sizetext-smtext-base md:text-sm
Styles object patternstyles.base above componentInline cn() inside JSX
File input stylingfile:* utility classes
aria-invalid destructive statearia-invalid:border-destructive built in
Focus ringfocus:outline-none transition-colorsfocus-visible:ring-[3px] ring-ring/50
Shadowshadow-xs
Selection colorBrowser defaultselection:bg-primary
LicenseMITMIT
Copy-paste install

API side-by-side

Both components forward every native input attribute through React.InputHTMLAttributes<HTMLInputElement>. Type, placeholder, value, defaultValue, onChange, onBlur, disabled, required, readOnly, autoComplete, min, max, pattern, name, id — every prop you would expect on <input> works on either side without a wrapper type. shadcn's newer recipe takes ref as a prop (React 19's ref-as-prop pattern), while Drivn keeps React.forwardRef for compatibility with older React majors and ref-typed forms in react-hook-form.

The call site looks the same in either library. Drop the component into a form, set type and placeholder, and wire onChange or register from your form library of choice. Where the surfaces diverge is the default class set. shadcn ships file-input styling, an aria-invalid border, and a focus-visible ring out of the box. Drivn ships none of those — they are local edits to styles.base if you want them. See the Input reference for the full Drivn surface.

1// shadcn/ui — long inline cn(), ref-as-prop
2import * as React from "react"
3import { cn } from "@/lib/utils"
4
5function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 return (
7 <input
8 type={type}
9 data-slot="input"
10 className={cn(
11 "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12 "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
13 "aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
14 className
15 )}
16 {...props}
17 />
18 )
19}
20
21// Drivn — styles.base above the component, forwardRef
22import * as React from 'react'
23import { cn } from '@/utils/cn'
24
25const styles = {
26 base: cn(
27 'w-full h-10 px-4 border border-input rounded-[10px]',
28 'text-foreground placeholder:text-muted-foreground text-sm',
29 'focus:outline-none transition-colors',
30 'disabled:opacity-50 disabled:cursor-default'
31 ),
32}
33
34type InputProps = React.InputHTMLAttributes<HTMLInputElement>
35
36export const Input = React.forwardRef<HTMLInputElement, InputProps>(({
37 className,
38 ...props
39}, ref) => (
40 <input ref={ref} className={cn(styles.base, className)} {...props} />
41 )
42)
43
44Input.displayName = 'Input'

Dependency footprint

Both components add zero runtime UI dependencies to your package.json. shadcn's Input imports only cn from its local utils file and the React types. Drivn's Input imports cn from @/utils/cn and React for forwardRef and the input attributes type. There is no Radix dependency, no Floating UI, no popper library, no form-state library underneath either one. Both files compile down to about a dozen lines of useful JSX plus a className string.

The practical difference is the className surface area shipped to your bundle. shadcn's default class string is roughly 380 characters because it covers file inputs, focus rings, aria-invalid, dark-mode backgrounds, and disabled pointer-events. Drivn's default is about 140 characters because it covers the four things every input needs — border, padding, text, disabled — and stops there. Tailwind will tree-shake the unused utilities either way, so the runtime cost difference is negligible. The readability difference at the source level is real: a 140-character constant in a styles object reads at a glance, a 380-character inline string does not.

1// Both libraries — package.json after installing the input component
2{
3 "dependencies": {
4 // no new dependencies added by either library
5 }
6}

Focus state and accessibility defaults

shadcn ships a visible focus ring out of the box. The recipe applies focus-visible:border-ring, focus-visible:ring-ring/50, and focus-visible:ring-[3px] so keyboard users see a clear three-pixel ring on the active input. It also wires an aria-invalid style: setting aria-invalid={true} on the input flips the border to border-destructive and adds a ring-destructive/20 aura, which means a server-validation error or a react-hook-form field error can flip the visual state without any extra className management on your side.

Drivn's default is focus:outline-none transition-colors — no ring, no border color change. The decision is intentional: most production inputs sit inside a Label+input+helper pattern where the label and helper text are the primary feedback channels, and the error state is styled at the field-wrapper level rather than baked into the input itself. If you want a focus ring on a Drivn Input, add one line to styles.base after install: focus:ring-2 focus:ring-ring focus:ring-offset-2. The file lives in your repo, so the edit stays local.

1// Drivn — adding a focus ring is a one-line edit to styles.base
2const styles = {
3 base: cn(
4 'w-full h-10 px-4 border border-input rounded-[10px]',
5 'text-foreground placeholder:text-muted-foreground text-sm',
6 'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
7 'transition-colors',
8 'disabled:opacity-50 disabled:cursor-default'
9 ),
10}

What Drivn does not ship by default

shadcn's Input bakes in three things Drivn deliberately leaves out. First, file-input styling: the file:text-foreground, file:inline-flex, file:h-7, file:border-0, file:bg-transparent, and file:text-sm file:font-medium utilities style the native file-chooser button on <input type="file">. Drivn applies no file-specific utilities — file inputs render with the browser default chooser button, which is intentional because file uploads in production typically use a custom drop-zone built on top of <input type="file"> and the unstyled native button works fine when hidden behind a Drivn Button.

Second, aria-invalid destructive styling — Drivn delegates error visuals to the field wrapper. Third, shadow-xs and a selection:bg-primary selection color — both cosmetic choices that did not earn their place in the minimum viable input. None of this is hard to add: every one of these patterns is a one-line addition to styles.base after install. The Drivn choice is to ship the floor and let you compose up from there, rather than ship the ceiling and trim down.

1// Drivn — adding aria-invalid destructive border in one line
2const styles = {
3 base: cn(
4 'w-full h-10 px-4 border border-input rounded-[10px]',
5 'text-foreground placeholder:text-muted-foreground text-sm',
6 'focus:outline-none transition-colors',
7 'disabled:opacity-50 disabled:cursor-default',
8 'aria-[invalid=true]:border-destructive'
9 ),
10}

Use cases — when each wins

Pick shadcn's Input when the defaults match what you would build anyway: a visible three-pixel focus ring, an aria-invalid destructive border that pairs with any form library, file-input styling for the rare native file picker, and the text-base md:text-sm pattern that prevents iOS Safari from zooming on focus. If your form-heavy app needs these patterns on every input and you do not want to maintain them in your own styles object, shadcn ships them as the default.

Pick Drivn's Input when you want a small, readable file with a clear styles.base object you can edit in seconds. The minimal default works for the common case — a form field next to a Label with a helper paragraph underneath — and every richer behavior is one line away. The 40-pixel default height and 10-pixel border radius pair with Drivn's Button and Select for a consistent form rhythm. If you want the shadcn ergonomics on top of Drivn, copy the three lines into your local styles.base after install — the libraries are close enough that the migration is cosmetic.

Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

Yes. Both components type the props as React.InputHTMLAttributes<HTMLInputElement> (or React.ComponentProps<"input">, which resolves to the same shape). Every native input attribute — type, value, defaultValue, onChange, onBlur, disabled, required, readOnly, name, id, autoComplete, min, max, step, pattern — works on either component without a wrapper or override. The only API-level difference is ref handling: Drivn uses React.forwardRef and shadcn's newer recipe takes ref as a prop directly, but both produce identical call sites for consumers.

The Drivn Input is a single <input> element — it does not accept a leftIcon or rightIcon prop because the component file is intentionally minimal. The standard pattern is to wrap the input in a relative container, render a Lucide icon with absolute left-3 top-1/2 -translate-y-1/2, and pad the input with pl-10 so the text starts past the icon. The wrapper lives in your form code, not in the Drivn Input itself. See the Input examples page for the full pattern.

The Drivn Input has no built-in error styling, which is intentional — error visuals usually belong at the field-wrapper level (label, input, helper paragraph) rather than on the input itself. Two options: add aria-[invalid=true]:border-destructive to styles.base after install and toggle aria-invalid on the input, or wrap the input in a field component that renders a red helper paragraph below when an error string is present. The second pattern is what most react-hook-form integrations use and gives you a label, the input, and the error message in one composable unit.

shadcn's default class string includes file:text-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium so that <input type="file"> renders with a styled native chooser button instead of the browser default. Drivn omits those utilities because production file uploads almost always use a custom drop-zone built on top of a hidden <input type="file"> and a styled Button, which makes the native chooser button irrelevant. If you do want a styled file chooser, the six utilities are a one-line addition to styles.base.

Yes. The Drivn Input file has no "use client" directive because it uses no client-only APIs — no useState, no useEffect, no event handlers attached in the component. You can render it directly inside a Server Component to capture form data through a Server Action, or import it into a Client Component when you need controlled state or react-hook-form integration. Next.js draws the client boundary based on the consumer's file, not the Input itself, so the same component works in both rendering modes.