Skip to content
cennge-ui
GitHubv0.3.1, changelog

Input

A one-line text field with its label, a hint or an error under it, an optional clear button and room for icons.

  • spredo-crypto-screenerCrypto screener
  • playwright-msi-scraperScraper on Playwright
Only lowercase letters, digits and dashes, like payload-cache
  1. The × shows while there is text; it empties the field and puts the caret back in it.
  2. Click anywhere in the box, on the icon too: the field takes focus.
  3. Fix the package name: the error under it turns back into the description.
Live: the component itself, not a picture of it.

Field marks

Import
import { Input } from "cennge-ui";
Size
3.2 KB gzip for this import, its dependencies aside
Pattern
Native <input> with a <label>
State
Controlled with value and onValueChange, or uncontrolled with defaultValue
SSR
Rendered on the server in the Next.js check; hydrates without a warning
Hint and error
Linked with aria-describedby; the error replaces the hint
Forms
Errors from <Form> show under the field by name
Tests
Unit tests

When to use#

Use an Input for one line of free text: a name, an email, a search. It renders a real <input>, so type, name, autoComplete, pattern and every other attribute reach it as they are, and onValueChange hands you the string without reading the event.

Similar#

  • Textarea takes several lines and grows with them.
  • NumberInput is for numbers: it refuses letters, steps with the arrows and formats by locale.
  • PasswordInput adds a show/hide button and a Caps Lock hint.
  • Combobox is a text field whose value must come from a list.

Validate when the field is left#

isInvalid and errorMessage are yours to set, so you choose when to check. Here the address is checked once focus has left the field, not while it is being typed.

Reports about payload-cache go here
validate-on-blur.tsx
import { useState } from "react";
import { Input } from "cennge-ui";

export default function ContactEmail() {
  const [email, setEmail] = useState("");
  const [isTouched, setTouched] = useState(false);
  const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);

  return (
    <Input
      type="email"
      label="Security contact"
      placeholder="dev@example.com"
      description="Reports about payload-cache go here"
      value={email}
      onValueChange={setEmail}
      onBlur={() => setTouched(true)}
      isInvalid={isTouched && !isValid}
      errorMessage="Enter an address like dev@example.com"
      className="w-full max-w-80"
    />
  );
}

Prefix and suffix#

startContent and endContent sit inside the box, before and after the text. A click on them still focuses the field.

github.com/Cennge/
https://.example.com
adornments.tsx
import { Input } from "cennge-ui";

export default function PreviewAddress() {
  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      <Input
        label="Repository"
        defaultValue="cennge-ui"
        startContent={<span className="text-sm text-muted select-none">github.com/Cennge/</span>}
      />
      <Input
        label="Preview address"
        defaultValue="payload-cache"
        startContent={<span className="text-sm text-muted select-none">https://</span>}
        endContent={<span className="text-sm text-muted select-none">.example.com</span>}
      />
    </div>
  );
}

Variants#

variant sets the box and color the focus colour, primary by default. size and radius take the same values as on Button.

variants.tsx
import { Input, type InputVariant } from "cennge-ui";

const variants: InputVariant[] = ["flat", "bordered", "faded", "underlined"];

export default function InputVariants() {
  return (
    <div className="grid w-full max-w-xl gap-x-6 gap-y-5 sm:grid-cols-2">
      {variants.map((variant) => (
        <Input key={variant} variant={variant} label={variant[0]!.toUpperCase() + variant.slice(1)} placeholder="ai-niche-scout" />
      ))}
    </div>
  );
}

Checked by the browser#

Inside a Form, the browser's own checks (isRequired, type="email", pattern) show their message under the field instead of in a tooltip, and a blocked submit focuses the first field to fix. Press Invite with the email empty.

Lowercase letters, digits and dashes
in-a-form.tsx
import { Button, Form, Input, toast } from "cennge-ui";

export default function InviteForm() {
  return (
    <Form
      className="flex w-full max-w-80 flex-col gap-4"
      onSubmit={(event) => {
        event.preventDefault();
        const data = new FormData(event.currentTarget);
        toast.success("Invitation sent", { description: `${data.get("email")} can now push to ${data.get("repository")}` });
      }}
    >
      <Input name="email" type="email" label="Email" placeholder="dev@example.com" isRequired />
      <Input
        name="repository"
        label="Repository"
        defaultValue="cennge-ui"
        isRequired
        pattern="[a-z0-9\-]+"
        description="Lowercase letters, digits and dashes"
      />
      <Button type="submit" color="primary" className="self-start">
        Invite
      </Button>
    </Form>
  );
}

Accessibility#

The label is a <label for> the input, and the description or the error is its aria-describedby, so a screen reader reads it with the field. isInvalid sets aria-invalid. isRequired sets the native required attribute; the asterisk after the label is drawn in CSS. The clear button is a real button named "Clear", reachable with Tab, and it hands focus back to the field.

Styling#

className goes to the outer wrapper, which holds the label, the box and the text under it. classNames reaches the parts inside: base, label, inputWrapper (the box), input, clearButton, description and errorMessage.

Props#

PropTypeDefault
label
React.ReactNode
–
description
React.ReactNode
–
errorMessage
React.ReactNodeShown instead of description while isInvalid.
–
variant
"bordered""flat""faded""underlined"
"flat"
color
"default""primary""secondary""success""warning""danger""info"Focus colour.
"primary"
size
"sm""md""lg"
"md"
radius
"none""sm""md""lg""full"
"md"
value
string
–
defaultValue
string
–
onValueChange
(value: string) => voidCalled with the new string value. Fires alongside onChange.
–
isInvalid
boolean
–
isRequired
boolean
–
isDisabled
boolean
–
isClearable
booleanShows a clear button while the field has a value.
–
onClear
() => void
–
startContent
React.ReactNode
–
endContent
React.ReactNode
–
classNames
Partial<Record<InputSlots, string>>Classes for inner parts. className goes to the outer wrapper.
–

Also takes every attribute of <input>, except size, color, value, defaultValue, which mean something else here.