bpdm/ui
Form

Input

A text field — accessible, themeable, in React (@bpdm/ui) and Angular (@bpdm/ng).

The Input is a text field built on the native <input>, so every native type, attribute and form behaviour works. In React it's the Input component; in Angular it's the bpdmInput attribute directive on a real <input>.

Usage

input-usage.tsx
import { Input } from '@bpdm/ui/input';

export function InputUsage() {
  return <Input placeholder="you@company.com" />;
}
input-usage.ts
import { Component } from '@angular/core';
import { BpdmInput } from '@bpdm/ng';

@Component({
  selector: 'input-usage',
  imports: [BpdmInput],
  templateUrl: './input-usage.html',
})
export class InputUsage {}
input-usage.html
<input bpdmInput placeholder="you@company.com" />

Variants

The variant prop offers outline (default, bordered box) and underline (a single bottom border).

input-variants.tsx
import { Input } from '@bpdm/ui/input';

export function InputVariants() {
  return (
    <div className="flex flex-col gap-3">
      <Input variant="outline" placeholder="Outline (default)" />
      <Input variant="underline" placeholder="Underline" />
    </div>
  );
}
input-variants.component.ts
import { Component } from '@angular/core';
import { BpdmInput } from '@bpdm/ng';

@Component({
  selector: 'app-input-variants',
  standalone: true,
  imports: [BpdmInput],
  template: `
    <div class="flex flex-col gap-3">
      <input bpdmInput variant="outline" placeholder="Outline (default)" />
      <input bpdmInput variant="underline" placeholder="Underline" />
    </div>
  `,
})
export class InputVariantsComponent {}

Sizes

The size prop offers sm, md (default), and lg.

input-sizes.tsx
import { Input } from '@bpdm/ui/input';

export function InputSizes() {
  return (
    <div className="flex flex-col gap-3">
      <Input size="sm" placeholder="Small" />
      <Input size="md" placeholder="Medium" />
      <Input size="lg" placeholder="Large" />
    </div>
  );
}
input-sizes.component.ts
import { Component } from '@angular/core';
import { BpdmInput } from '@bpdm/ng';

@Component({
  selector: 'app-input-sizes',
  standalone: true,
  imports: [BpdmInput],
  template: `
    <div class="flex flex-col gap-3">
      <input bpdmInput size="sm" placeholder="Small" />
      <input bpdmInput size="md" placeholder="Medium" />
      <input bpdmInput size="lg" placeholder="Large" />
    </div>
  `,
})
export class InputSizesComponent {}

States

Use the native attributes — disabled, a value, and aria-invalid for the error state (the field turns destructive-coloured automatically).

input-states.tsx
import { Input } from '@bpdm/ui/input';

export function InputStates() {
  return (
    <div className="flex flex-col gap-3">
      <Input placeholder="Default" />
      <Input defaultValue="With a value" />
      <Input aria-invalid defaultValue="Invalid value" />
      <Input disabled placeholder="Disabled" />
    </div>
  );
}
input-states.component.ts
import { Component } from '@angular/core';
import { BpdmInput } from '@bpdm/ng';

@Component({
  selector: 'app-input-states',
  standalone: true,
  imports: [BpdmInput],
  template: `
    <div class="flex flex-col gap-3">
      <input bpdmInput placeholder="Default" />
      <input bpdmInput value="With a value" />
      <input bpdmInput aria-invalid="true" value="Invalid value" />
      <input bpdmInput disabled placeholder="Disabled" />
    </div>
  `,
})
export class InputStatesComponent {}

With icons

In React, add a leading or trailing icon with startIcon / endIcon and the field pads itself to clear them.

input-icons.tsx
import { Search, Calendar, Mail, Lock } from 'lucide-react';
import { Input } from '@bpdm/ui/input';

export function InputIcons() {
  return (
    <div className="flex flex-col gap-3">
      <Input placeholder="Search" startIcon={<Search />} />
      <Input placeholder="Pick a date" endIcon={<Calendar />} />
      <Input type="email" placeholder="Email" startIcon={<Mail />} />
      <Input type="password" placeholder="Password" startIcon={<Lock />} />
    </div>
  );
}

startIcon / endIcon are a React convenience. In Angular, wrap the input and position the icon, padding the field to clear it:

input-icons.component.ts
import { Component } from '@angular/core';
import { BpdmInput } from '@bpdm/ng';

@Component({
  selector: 'app-input-icons',
  standalone: true,
  imports: [BpdmInput],
  // logical `start-3` / `ps-9` so the icon + padding mirror under dir="rtl"
  template: `
    <div class="relative flex items-center">
      <svg class="pointer-events-none absolute start-3 size-4 text-muted-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
        <circle cx="11" cy="11" r="7" />
        <path d="m21 21-4.3-4.3" />
      </svg>
      <input bpdmInput class="ps-9" placeholder="Search" />
    </div>
  `,
})
export class InputIconsComponent {}

Input types

Any native type works — email, password, number, and the rest.

input-types.tsx
import { Input } from '@bpdm/ui/input';

export function InputTypes() {
  return (
    <div className="flex flex-col gap-3">
      <Input type="email" placeholder="email@company.com" />
      <Input type="password" placeholder="••••••••" />
      <Input type="number" placeholder="0" />
    </div>
  );
}
input-types.component.ts
import { Component } from '@angular/core';
import { BpdmInput } from '@bpdm/ng';

@Component({
  selector: 'app-input-types',
  standalone: true,
  imports: [BpdmInput],
  template: `
    <div class="flex flex-col gap-3">
      <input bpdmInput type="email" placeholder="email@company.com" />
      <input bpdmInput type="password" placeholder="••••••••" />
      <input bpdmInput type="number" placeholder="0" />
    </div>
  `,
})
export class InputTypesComponent {}

Form field

Pair the input with a <label> (linked via htmlFor/for) and an optional helper line. This is the accessible, recommended pattern.

We'll never share your email.

input-field.tsx
import { Mail } from 'lucide-react';
import { Input } from '@bpdm/ui/input';

export function InputField() {
  return (
    <div className="flex flex-col gap-1.5">
      <label htmlFor="email" className="text-sm font-medium">
        Email
      </label>
      <Input id="email" type="email" placeholder="you@company.com" startIcon={<Mail />} />
      <p className="text-xs text-muted-foreground">We'll never share your email.</p>
    </div>
  );
}
input-field.component.ts
import { Component } from '@angular/core';
import { BpdmInput } from '@bpdm/ng';

@Component({
  selector: 'app-input-field',
  standalone: true,
  imports: [BpdmInput],
  template: `
    <div class="flex flex-col gap-1.5">
      <label for="email" class="text-sm font-medium">Email</label>
      <input bpdmInput id="email" type="email" placeholder="you@company.com" />
      <p class="text-xs text-muted-foreground">We'll never share your email.</p>
    </div>
  `,
})
export class InputFieldComponent {}

API

PropTypeDefaultDescription
variantoutline | underlineoutlineBordered box, or a single bottom border.
sizesm | md | lgmdField height and text size.
startIconReactNodeReact only — icon rendered at the leading edge.
endIconReactNodeReact only — icon rendered at the trailing edge.

All native <input> attributes (type, placeholder, value, disabled, readOnly, aria-invalid, …) are supported. In Angular, variant and size are @Inputs on the input[bpdmInput] directive.

Accessibility

  • Always pair the input with a <label> linked by htmlFor (React) / for (Angular), or give it an aria-label.
  • Set aria-invalid when a field fails validation — bpdm styles it with the destructive colour, and screen readers announce the error state.
  • It's a native <input>, so keyboard and assistive-tech behaviour are built in.

Internationalization

  • The input renders no text of its own — pass a translated placeholder, a translated accessible name (associated <label> or aria-label), and translated help/error via aria-describedby.
  • RTL-safe: the start/end icon padding uses logical properties (ps/pe, start/end), so the field and its icons mirror under dir="rtl". See the Internationalization guide.

On this page