Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions apps/engineering/content/design/components/form-input.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
title: FormInput
description: A form input component with built-in label, description, and error handling capabilities.
---
import {
DefaultFormInputVariant,
RequiredFormInputVariant,
SuccessFormInputVariant,
WarningFormInputVariant,
ErrorFormInputVariant,
DisabledFormInputVariant,
DefaultValueFormInputVariant,
ReadonlyFormInputVariant,
ComplexFormInputVariant
} from "./form/form-input.variants"

# FormInput
A comprehensive form input component that combines labels, descriptions, and validation states. Perfect for creating accessible, user-friendly forms with proper labeling and helpful context.

## Default
The default FormInput includes a label and optional description text, providing clear context for users.

<DefaultFormInputVariant />

## Input States

### Required Field
Use the required prop to indicate mandatory fields. This automatically adds an asterisk (*) to the label.

<RequiredFormInputVariant />

### Success State
Indicates successful validation or acceptance of input value. The success icon and text provide positive feedback.

<SuccessFormInputVariant />

### Warning State
Used for potentially problematic inputs that don't prevent form submission. Includes a warning icon and explanatory text.

<WarningFormInputVariant />

### Error State
Shows validation errors or other issues that need user attention. Features prominent error styling and message.

<ErrorFormInputVariant />

### Disabled State
Apply when the field should be non-interactive, such as during form submission or based on other field values.

<DisabledFormInputVariant />

### With Default Value
Pre-populated input with an initial value that users can modify.

<DefaultValueFormInputVariant />

### Read-only State
For displaying non-editable information while maintaining form layout consistency.

<ReadonlyFormInputVariant />

## Complex Usage
Example of a FormInput with multiple props configured for a specific use case.

<ComplexFormInputVariant />

## Props
The FormInput component extends the standard Input component props with additional form-specific properties:

<AutoTypeTable
name="FormInputProps"
type={`import { InputProps } from "../input"
import { ReactNode } from "react"
export interface FormInputProps extends InputProps {
/** Text label for the input field */
label?: string;
/** Helper text providing additional context */
description?: string;
/** Whether the field is required */
required?: boolean;
/** Error message to display when validation fails */
error?: string;
/** ID for the input element, auto-generated if not provided */
id?: string;
/** Additional class names to apply to the fieldset wrapper */
className?: string;
/** Visual state variant passed to the underlying Input component */
variant?: 'default' | 'success' | 'warning' | 'error';
}`}
/>

## Accessibility
FormInput is built with accessibility in mind:
- Labels are properly associated with inputs using htmlFor/id
- Error messages are announced to screen readers using role="alert"
- Required fields are marked both visually and via aria-required
- Helper text is linked to inputs using aria-describedby
- Error states are indicated using aria-invalid

## Best Practices
When using the FormInput component:
- Always provide clear, concise labels
- Use description text to provide additional context when needed
- Keep error messages specific and actionable
- Use required fields sparingly and logically
- Group related FormInputs using fieldset and legend when appropriate
- Consider the mobile experience when writing labels and descriptions
- Maintain consistent validation patterns across your form
- Use appropriate input types (email, tel, etc.) for better mobile keyboards
- Consider character/word limits in descriptions and error messages
- Test with screen readers to ensure accessibility

## Layout Guidelines
- Labels should be clear and concise
- Error messages should appear immediately below the input
- Description text should be helpful but not too lengthy
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { RenderComponentWithSnippet } from "@/app/components/render";
import { FormInput } from "@unkey/ui";

export const DefaultFormInputVariant = () => {
return (
<RenderComponentWithSnippet>
<FormInput
label="Username"
description="Choose a unique username for your account"
placeholder="e.g. gandalf_grey"
/>
</RenderComponentWithSnippet>
);
};

// Required field variant
export const RequiredFormInputVariant = () => {
return (
<RenderComponentWithSnippet>
<FormInput
label="Email Address"
description="We'll send your confirmation email here"
required
placeholder="frodo@shire.me"
/>
</RenderComponentWithSnippet>
);
};

// Success variant
export const SuccessFormInputVariant = () => {
return (
<RenderComponentWithSnippet>
<FormInput
label="API Key"
description="Your API key has been verified"
variant="success"
defaultValue="sk_live_middleearth123"
placeholder="Enter your API key"
/>
</RenderComponentWithSnippet>
);
};

// Warning variant
export const WarningFormInputVariant = () => {
return (
<RenderComponentWithSnippet>
<FormInput
label="Password"
description="Your password is about to expire"
variant="warning"
type="password"
placeholder="Enter your password"
/>
</RenderComponentWithSnippet>
);
};

// Error variant
export const ErrorFormInputVariant = () => {
return (
<RenderComponentWithSnippet>
<FormInput
label="Repository Name"
error="A repository with this name already exists"
placeholder="my-awesome-project"
/>
</RenderComponentWithSnippet>
);
};

// Disabled variant
export const DisabledFormInputVariant = () => {
return (
<RenderComponentWithSnippet>
<FormInput
label="Organization ID"
description="Contact admin to change organization ID"
disabled
defaultValue="org_fellowship123"
placeholder="Organization ID"
/>
</RenderComponentWithSnippet>
);
};

// With default value
export const DefaultValueFormInputVariant = () => {
return (
<RenderComponentWithSnippet>
<FormInput
label="Project Name"
description="Name of your new project"
defaultValue="The Fellowship Project"
placeholder="Enter project name"
/>
</RenderComponentWithSnippet>
);
};

// Readonly variant
export const ReadonlyFormInputVariant = () => {
return (
<RenderComponentWithSnippet>
<FormInput
label="Generated Token"
description="Copy this token for your records"
readOnly
defaultValue="tkn_1ring2rulethemall"
placeholder="Your token will appear here"
/>
</RenderComponentWithSnippet>
);
};

// Complex example with multiple props
export const ComplexFormInputVariant = () => {
return (
<RenderComponentWithSnippet>
<FormInput
label="Webhook URL"
description="Enter the URL where we'll send event notifications"
required
placeholder="https://api.yourdomain.com/webhooks"
className="max-w-lg"
id="webhook-url-input"
/>
</RenderComponentWithSnippet>
);
};
2 changes: 0 additions & 2 deletions apps/engineering/content/design/components/input.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
title: Input
description: A text input field component with different states, validations, and icon support.
---

import { Input } from "@unkey/ui"
import { RenderComponentWithSnippet } from "@/app/components/render"
import {
InputDefaultVariant,
Expand Down
82 changes: 82 additions & 0 deletions internal/ui/src/components/form/form-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { CircleInfo, TriangleWarning2 } from "@unkey/icons";
import * as React from "react";
import { cn } from "../../lib/utils";
import { Input, type InputProps } from "../input";

export interface FormInputProps extends InputProps {
label?: string;
description?: string;
required?: boolean;
error?: string;
}

export const FormInput = React.forwardRef<HTMLInputElement, FormInputProps>(
({ label, description, error, required, id, className, variant, ...props }, ref) => {
const inputVariant = error ? "error" : variant;

const inputId = id || React.useId();
const descriptionId = `${inputId}-helper`;
const errorId = `${inputId}-error`;

return (
<fieldset className={cn("flex flex-col gap-1.5 border-0 m-0 p-0", className)}>
{label && (
<label
id={`${inputId}-label`}
htmlFor={inputId}
className="text-gray-11 text-[13px] flex items-center"
>
{label}
{required && (
<span className="text-error-9 ml-1" aria-label="required field">
*
</span>
)}
</label>
)}

<Input
ref={ref}
id={inputId}
variant={inputVariant}
aria-describedby={error ? errorId : description ? descriptionId : undefined}
aria-invalid={!!error}
aria-required={required}
{...props}
/>

{(description || error) && (
<div className="text-[13px] leading-5">
{error ? (
<div id={errorId} role="alert" className="text-error-11 flex gap-2 items-center">
<TriangleWarning2 aria-hidden="true" />
{error}
</div>
) : description ? (
<output
id={descriptionId}
className={cn(
"text-gray-9 flex gap-2 items-center",
variant === "success"
? "text-success-11"
: variant === "warning"
? "text-warning-11"
: "",
)}
>
{variant === "warning" ? (
<TriangleWarning2 size="md-regular" aria-hidden="true" />
) : (
<CircleInfo size="md-regular" aria-hidden="true" />
)}
<span>{description}</span>
</output>
) : null}
</div>
)}
</fieldset>
);
},
);

FormInput.displayName = "FormInput";
1 change: 1 addition & 0 deletions internal/ui/src/components/form/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./form-input";
12 changes: 6 additions & 6 deletions internal/ui/src/components/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ const inputVariants = cva(
"[&:not(:placeholder-shown)]:focus:ring-0",
],
success: [
"border border-success-6 hover:border-success-7 bg-gray-2",
"border border-success-9 hover:border-success-10 bg-gray-2",
"focus:border-success-8 focus:ring-2 focus:ring-success-2 focus-visible:outline-none",
"[&:not(:placeholder-shown)]:focus:ring-success-3",
"[&:not(:placeholder-shown)]:focus:ring-success-0",
],
warning: [
"border border-warning-6 hover:border-warning-7 bg-gray-2",
"border border-warning-9 hover:border-warning-10 bg-gray-2",
"focus:border-warning-8 focus:ring-2 focus:ring-warning-2 focus-visible:outline-none",
"[&:not(:placeholder-shown)]:focus:ring-warning-3",
"[&:not(:placeholder-shown)]:focus:ring-warning-0",
],
error: [
"border border-error-6 hover:border-error-7 bg-gray-2",
"border border-error-9 hover:border-error-10 bg-gray-2",
"focus:border-error-8 focus:ring-2 focus:ring-error-2 focus-visible:outline-none",
"[&:not(:placeholder-shown)]:focus:ring-error-3",
"[&:not(:placeholder-shown)]:focus:ring-error-0",
],
},
},
Expand Down
1 change: 1 addition & 0 deletions internal/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from "./components/tooltip";
export * from "./components/date-time/date-time";
export * from "./components/input";
export * from "./components/empty";
export * from "./components/form";
Loading