# ljkui — full documentation > ljkui — a React component library. Themeable, ESM-only, ships its own CSS. Import components from `ljkui`, import `ljkui/styles.css` once, wrap your app in ``. Start with llms.txt if you have not read it — it has the setup and the rules. --- # Guides ## Adopting tokens ljkui is a token-first library. Instead of hard-coding hex colors and one-off pixel values, you reach for the design tokens — Tailwind-style utilities like `bg-accent-700`, `text-gray-900`, `border-gray-alpha-300`, and the matching CSS custom properties `var(--accent-700)`, `var(--gray-900)`. Every palette ships as a 12-step scale whose steps are semantic **roles** (10–50 backgrounds, 100–300 fills, 400–600 borders, 700–800 solid, 900–950 text), so a token keeps its meaning across themes and light/dark — a raw hex never does. To make that migration painless there's a small checker/codemod, `lint-raw-colors`, that scans your source for raw hex colors and tells you the nearest token. ## What it does - Recursively scans `.ts` / `.tsx` / `.js` / `.jsx` / `.css` files. - Finds raw hex colors (`#rgb`, `#rrggbb`, `#rrggbbaa`) in classNames, `style` objects, and CSS values. - For each one, computes the **nearest token by color distance** — it converts the hex to CIELAB and finds the closest step across the shipped palettes, using the real seed values extracted from the library's `palettes.css` at build time (so it never goes stale against the library). - Reports `file:line:col #hex → utility / var(...) [ΔE distance]`. A chromatic match is suggested as the configurable `accent` scale (what you usually want); a neutral match is suggested as `gray`. - With `--fix`, conservatively rewrites the unambiguous className cases in place — `className="bg-[#3b82f6]"` becomes `className="bg-accent-800"`. Bare hex in a `style` object or CSS value is reported but never auto-edited. - Exits non-zero when anything is found, so you can gate CI — unless you pass `--report-only`. ## Running it The tool ships **with the `ljkui` package** as a bin, so you can run it in your own project with no setup: ```sh # scan your app (default path "."): bunx ljkui-lint-raw-colors src # just report, don't fail CI: bunx ljkui-lint-raw-colors src --report-only # rewrite the confident className cases: bunx ljkui-lint-raw-colors src --fix # also flag arbitrary Tailwind spacing (p-[13px] → p-3): bunx ljkui-lint-raw-colors src --spacing ``` Working in the ljkui repo itself? Run the same tool from source: ```sh bun src/bin/lint-raw-colors.ts ../../path/to/your/app/src ``` Example output: ```text src/Card.tsx:12:20 #3b82f6 → bg-accent-800 / var(--accent-800) [ΔE 18.5] src/Card.tsx:14:11 #111827 → bg-gray-950 / var(--gray-950) [ΔE 4.2] src/theme.css:3:15 #f11325 → bg-accent-700 / var(--accent-700) [ΔE 0.0 (exact)] 3 raw color(s) across 1 file(s). ``` ## The token-first philosophy A raw hex encodes one appearance. A token encodes an **intent** — "solid accent", "subtle border", "body text" — and resolves to the right value for the active theme, for light and dark, and for whatever `accentColor` the consumer picks. Adopting tokens is mostly a search-and-replace once you know the mapping, and that mapping is exactly what this tool prints. Wire it into CI in `--report-only` mode first to see the scope, then drop `--report-only` once you're clean. --- > **Maintainer note:** this guide is not yet wired into the Storybook `GUIDES` array in > `scripts/generate-storybook.ts` — add `adopting-tokens` there to surface it. --- ## Breakpoints Breakpoints allow you to build responsive layouts based on different screen sizes. ## Available breakpoints Breakpoints are `min-width` based (mobile-first) and apply when the screen width is equal or greater than given breakpoint.
Size Screen Width
initial Phones (portrait) 0px
xs Phones (landscape) 520px
sm Tablets (portrait) 768px
md Tablets (landscape) 1024px
lg Laptops 1280px
xl Desktops 1640px
--- ## Color system The color palettes in ljkui are the [Tailwind CSS v4 palettes](https://tailwindcss.com/docs/colors), each extended with an extra light shade to form a 12-step scale with light, dark and alpha variants. Colors are grouped into 3 categories: `accents`, `grays`, and `semantic` (info, success, warning, danger). All of these can be specified on your ``, or per component where appropriate. ## Understanding the scale Steps are named after the Tailwind stops, with an extra `10` step for the additional light shade — so a scale reads `10, 50, 100, 200, … 900, 950`, and `accent-950` is the darkest step. Each scale has 12 steps with dedicated roles — `10`-`50` backgrounds, `100`-`300` component backgrounds, `400`-`600` borders, `700`-`800` solid colors, `900`-`950` text. **A step's name is not the Tailwind stop it is built from**, in either light or dark mode. `accent-500` is a *border*, so it is derived from around Tailwind's `300`, not its `500`. The scales are Tailwind palettes fitted onto these roles, not Tailwind's ramp renamed. Six of the twelve steps are backgrounds and borders, so they all live in the light half of the palette; the solid step is the vivid form of the hue and is the *same color* in light and dark; and the `900` text step is solved for a contrast ratio against `10` rather than pinned to a stop. If you want Tailwind's stop values themselves, use Tailwind's own `blue-500` utility — `accent-500` is a role. ## Solid and alpha Every scale ships twice. `--accent-200` is opaque; `--accent-alpha-200` is the same step as a translucent color, so it **tints** whatever is behind it instead of covering it — over the page background it lands on exactly the solid step, and over a panel it keeps its hue. This is why the alpha scale exists rather than an opacity modifier: `bg-accent-alpha-200` is a saturated color at low alpha, whereas `bg-accent-200/50` fades the already-pale solid step toward whatever is behind it, which goes grey on dark backgrounds. Use the alpha steps for anything layered over a surface — soft variants, hover states, borders. ```tsx
// opaque
// tints the surface underneath ``` --- ## Forms & Field Binding Every ljkui input is a thin, styled wrapper over a native control. The library owns the look — you own the state. That means an input binds to whatever holds your form state (React `useState`, a native `
`, or a form library) through one small, predictable prop contract, and never depends on the form library itself. The `ljkui` package ships **no** form dependency. `@tanstack/react-form` is a dev dependency used only by the examples — you bring your own. ## The binding contract Each input exposes a controlled/uncontrolled surface plus the native form attributes: | Concern | Text-like inputs | Boolean inputs (Checkbox, Switch) | Choice inputs (Select, RadioGroup) | | --- | --- | --- | --- | | Controlled value | `value` | `checked` | `value` | | Uncontrolled default | `defaultValue` | `defaultChecked` | `defaultValue` | | Change callback | `onValueChange` / native `onChange` | `onCheckedChange` | `onValueChange` | | Submission key | `name` | `name` | `name` | | Disabled | `disabled` | `disabled` | `disabled` | | Required | `required` | `required` | `required` | `Input.Control` forwards native DOM props, so `value` + `onChange` (the native event) work exactly as they do on a plain ``; the higher-level controls (Checkbox, Switch, Select) expose the semantic `onCheckedChange` / `onValueChange` callbacks. ### Error / invalid state Validation state lives on the **wrapper**, not the input. `` and `` own the invalid state and the error message: - `` — mark the field invalid from an external source (a form library, a server response). This is the escape hatch when you are not using native constraint validation. - `` — render an error keyed to a native validity state (`valueMissing`, `typeMismatch`, `tooShort`, `patternMismatch`, …). - `` — always render when the field is invalid, which is how you surface a message string that came from a form library. - `` — non-error helper text. ```tsx Email Email is required Please enter a valid email ``` ## Controlled vs uncontrolled **Uncontrolled** is the default and the least code. Wrap fields in `` (a native `` with consolidated error handling) and read values on submit — either from the native event or from the `onFormSubmit` callback, which hands you a plain object: ```tsx console.log(values)}> Name ``` Native constraint validation (`required`, `minLength`, `pattern`, …) plus `` covers most forms with no JavaScript state at all. **Controlled** is what you reach for when the value drives other UI (conditional fields, live previews, debounced auto-save) or when a form library owns the state: ```tsx const [country, setCountry] = React.useState('us'); v && setCountry(v)} items={items}> ; ``` ## Binding a form library (TanStack Form) Because the contract is just `value` + a change callback + an `invalid` flag, any form library binds cleanly. With `@tanstack/react-form`, `form.Field` is a render prop that supplies the field state; map it onto the input and forward errors into ``: ```tsx import { useForm } from '@tanstack/react-form'; import { Button, Field, Input } from 'ljkui'; function ProfileForm() { const form = useForm({ defaultValues: { username: '' }, onSubmit: async ({ value }) => save(value), }); return (
{ e.preventDefault(); e.stopPropagation(); form.handleSubmit(); }} > value.length < 3 ? 'Username must be at least 3 characters' : undefined, }} > {(field) => ( 0}> Username field.handleChange(e.target.value)} onBlur={field.handleBlur} /> {field.state.meta.errors.length > 0 && ( {field.state.meta.errors[0]} )} )} state.isSubmitting}> {(isSubmitting) => ( )}
); } ``` The three moving parts are always the same, whatever the library: 1. **Value in** — feed the library's value into `value` (or `checked`). 2. **Change out** — call the library's change handler from `onChange` / `onValueChange` / `onCheckedChange`. 3. **Errors** — set `invalid` on `` and render `` with the library's message. Boolean and choice controls follow the same shape — bind `checked`/`onCheckedChange` for `` and ``, and `value`/`onValueChange` for `