- Open an issue first before working on new components or significant changes
- For new components, use the Component Proposal Template below
- Keep PRs small — one component or feature per PR
bun install
bun dev # dev server
bun build # production buildtype(scope): message
Types: feat, fix, refactor, docs, test, chore
Examples:
feat(radio-group): add error state propfix(dropdown-select): return focus to trigger on close
Every component MUST follow these rules. This is non-negotiable.
src/components/component-name/
ComponentName.tsx # Implementation
index.ts # Barrel export
Barrel export pattern:
export { default as ComponentName, type ComponentNameProps } from "./ComponentName";- Extend
IComponentBasePropswhen the component needsdataThemesupport. All components should acceptclassandstyleat minimum. - Use
splitPropsto separate component props from HTML pass-through - Use
twMerge()for class merging — never string concatenation. Addclsx()inside only when you have conditional classes (e.g.,twMerge(clsx({"btn-active": isActive}), local.class)) - Boolean props default to
false - Use
ComponentSizefor sizes (xs | sm | md | lg | xl) - Use
ComponentColorfor colors (primary | secondary | accent | info | success | warning | error) - Events:
onChange,onValueChange— pass the value, not the raw event - Accept
JSX.Elementfor labels/content when consumers might need rich content
- Interactive components MUST have
aria-labeloraria-labelledby - Use semantic HTML (
<button>,<fieldset>,<dialog>) — not styled divs - Keyboard navigation: Tab, Arrow keys, Enter/Space, Escape where applicable
-
aria-describedbyfor descriptions and error messages - Focus visible styles on all interactive elements
-
@media (prefers-reduced-motion: reduce)for animations -
roleattributes where HTML semantics are insufficient
-
functioncomponent with explicit return type: JSX.Element - Follow existing export pattern —
export defaultin component file, re-export as named in barrelindex.ts - No hardcoded English strings — accept as props with sensible defaults
- No inline
style={{}}when a Tailwind class exists — use classes instead. Dynamic values (animations, user-controlled sizes, calculated positions) are OK. - No
classNamedual support — useclassonly (SolidJS convention).classNameis accepted viaIComponentBasePropsfor compat butclassis canonical. - No
anytypes — use proper TypeScript types - Comments explain why, not what
A component does NOT belong in this library if:
- It is a styled div with fewer than 3 props of real behavior
- It hardcodes application-specific logic (user roles, session IDs, API calls)
- It duplicates an existing component with fewer features
- It is a layout shortcut that consumers can compose with
Flex+ existing primitives - No major UI library (Radix, Chakra, Mantine, Ant Design, MUI) ships an equivalent
- Every component MUST have a showcase page in js.software
- Showcase includes: default, variants, sizes, interactive examples, Props table
- Follow the existing pattern in
src/components/*Showcase.tsx
Open an issue with this template:
## Component Proposal: [Name]
### What it does
One sentence.
### Why it belongs in the library
- Which apps need it?
- Does an equivalent exist in Radix/Chakra/Mantine/Ant Design?
- Can it be composed from existing primitives instead?
### Proposed API
```tsx
<ComponentName
prop1="value"
prop2={true}
onChange={(value) => {}}
/>- ARIA pattern it follows (link to WAI-ARIA APG)
- Keyboard interactions planned
Why not use existing components or compose from primitives?
## PR Checklist
Before submitting:
- [ ] Component follows all rules in the [Component Checklist](#component-checklist)
- [ ] `bun build` passes
- [ ] No TypeScript errors (`npx tsc --noEmit`)
- [ ] Showcase page created/updated in js.software
- [ ] Commit message follows convention