jtl-ui-components
v1.0.4
Published
A modern, accessible React component library built on [Ark UI](https://ark-ui.com/) primitives and styled with [Tailwind CSS v4](https://tailwindcss.com/). Includes built-in dark mode support via [next-themes](https://github.com/pacocoursey/next-themes).
Readme
jtl-ui-components
A modern, accessible React component library built on Ark UI primitives and styled with Tailwind CSS v4. Includes built-in dark mode support via next-themes.
Installation
npm install jtl-ui-components
# or
pnpm add jtl-ui-components
# or
yarn add jtl-ui-componentsRequirements
- React >= 19
- Tailwind CSS v4
- next-themes (for
ThemeProvider/ThemeToggle)
Tailwind Setup
Because the library ships pre-built JS, you need to tell Tailwind to scan its source for utility classes. Add a @source directive pointing at the installed package in your global CSS file:
/* app/globals.css */
@import "tailwindcss";
@source "../node_modules/jtl-ui-components/dist/**/*.js";
/* Required for class-based dark mode (next-themes uses .dark on <html>) */
@variant dark (&:where(.dark, .dark *));Dark Mode Setup
Wrap your app with ThemeProvider in your root layout:
// app/layout.tsx
import { ThemeProvider } from 'jtl-ui-components';
import './globals.css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}Then place ThemeToggle anywhere to switch between light and dark:
import { ThemeToggle } from 'jtl-ui-components';
<ThemeToggle />Components
Button
import { Button } from 'jtl-ui-components';
<Button>Click me</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="destructive" size="lg">Delete</Button>
<Button variant="ghost" size="sm">Learn more</Button>| Prop | Type | Default | Description |
|---|---|---|---|
| variant | primary | secondary | ghost | destructive | primary | Visual style |
| size | sm | md | lg | md | Button size |
Extends all native <button> attributes.
Input
import { Input } from 'jtl-ui-components';
<Input label="Email" type="email" placeholder="[email protected]" />
<Input label="Username" helperText="Must be unique." />
<Input label="Username" error="This username is already taken." />| Prop | Type | Description |
|---|---|---|
| label | string | Field label |
| error | string | Error message (shown in red, marks field invalid) |
| helperText | string | Helper text shown below the field |
Extends all native <input> attributes.
PasswordInput
Like Input but with a built-in show/hide password toggle button.
import { PasswordInput } from 'jtl-ui-components';
<PasswordInput label="Password" placeholder="Create a strong password" />
<PasswordInput label="Password" error="Password is too short." />| Prop | Type | Description |
|---|---|---|
| label | string | Field label |
| error | string | Error message |
| helperText | string | Helper text |
Extends all native <input> attributes except type (managed internally).
Textarea
import { Textarea } from 'jtl-ui-components';
<Textarea label="Bio" placeholder="Tell us about yourself…" rows={4} />
<Textarea label="Bio" helperText="Max 300 characters." />
<Textarea label="Bio" error="Bio is required." />| Prop | Type | Description |
|---|---|---|
| label | string | Field label |
| error | string | Error message |
| helperText | string | Helper text |
Extends all native <textarea> attributes.
DateTimePicker
import { DateTimePicker } from 'jtl-ui-components';
<DateTimePicker label="Date of Birth" placeholder="Select a date" />
// Controlled
<DateTimePicker
label="Date of Birth"
value={value}
onValueChange={(val) => setValue(val)}
error={error}
/>| Prop | Type | Description |
|---|---|---|
| label | string | Field label |
| placeholder | string | Input placeholder |
| value | string | Controlled date value (ISO string) |
| onValueChange | (value: string) => void | Called when the date changes |
| error | string | Error message |
| helperText | string | Helper text |
Toggle
import { Toggle } from 'jtl-ui-components';
// Uncontrolled
<Toggle label="Enable notifications" defaultChecked />
// Controlled
<Toggle
label="Enable notifications"
checked={enabled}
onChange={(checked) => setEnabled(checked)}
/>
<Toggle label="Disabled" disabled />| Prop | Type | Description |
|---|---|---|
| label | string | Label text shown beside the switch |
| checked | boolean | Controlled checked state |
| defaultChecked | boolean | Initial state (uncontrolled) |
| disabled | boolean | Disables the toggle |
| onChange | (checked: boolean) => void | Called when state changes |
Avatar
import { Avatar } from 'jtl-ui-components';
// With image
<Avatar src="https://example.com/photo.jpg" alt="Jane Doe" size="md" />
// With fallback initials (shown when image fails or src is empty)
<Avatar fallback="JD" size="lg" />| Prop | Type | Default | Description |
|---|---|---|---|
| src | string | — | Image URL |
| alt | string | '' | Image alt text |
| fallback | string | '?' | Text shown when image is unavailable |
| size | sm | md | lg | xl | md | Avatar size |
Usage with react-hook-form
Native inputs (Input, PasswordInput, Textarea) work directly with register. Controlled components (DateTimePicker, Toggle) use Controller:
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Input, PasswordInput, Toggle, DateTimePicker } from 'jtl-ui-components';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
dateOfBirth: z.string().min(1),
notifications: z.boolean(),
});
function MyForm() {
const { register, control, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
});
return (
<form onSubmit={handleSubmit(console.log)}>
<Input label="Email" error={errors.email?.message} {...register('email')} />
<PasswordInput label="Password" error={errors.password?.message} {...register('password')} />
<Controller
name="dateOfBirth"
control={control}
render={({ field }) => (
<DateTimePicker
label="Date of Birth"
onValueChange={field.onChange}
error={errors.dateOfBirth?.message}
/>
)}
/>
<Controller
name="notifications"
control={control}
render={({ field }) => (
<Toggle
label="Email notifications"
checked={field.value}
onChange={field.onChange}
/>
)}
/>
</form>
);
}License
MIT
